VUE知识点补充
v-if、v-show 切换true/false时,分别触发哪些生命周期
v-show和v-if都能控制元素的显示与隐藏。
控制手段不同:
v-show 通过控制CSS的display为none来隐藏
v-if 是动态的向DOM树添加或删除元素
编译过程不同:
v-show 是通过向dom元素添加css属性 display:none实现是否隐藏
v-if是通过局部的编译/卸载实现切换,切换过程中会合适的销毁和重建内部监听事件和子组件
编译的条件不同(触发生命周期)
v-show在true和false的切换中,不会触发生命周期
v-if是真正的条件渲染,能够保证在切换的过程中适当销毁和重建内部监听事件和子组件
1.v-if在true变为false的时候卸载dom,触发beforeDestoryed和destoryed
2.v-if在false变成true的时候重建dom,触发beforeCreate、create、beforeMount、mounted
使用场景和性能消耗不同
如果要频繁的切换某节点,使用v-show,初始开销大,切换开销小
如果不需要频繁的切换,使用v-if,初始渲染开销小,切换开销大
v-if 、v-for哪个优先级高
在vue2中,v-for的优先级高于v-if;在vue3中,v-if的优先级高于v-for。在vue中,永远不要把v-if和v-for同时用在同一个元素上,会带来性能方面的浪费(每次渲染都会先循环再进行条件判断);
1.想要避免出现这种情况,可在外层嵌套template(页面渲染不生成dom节点),在这一层进行v-if判断,然后在内部进行v-for循环。
<template v-if="isShow">
<p v-for="item in items">
</template>
2.如果条件出现在循环内部,可通过计算属性computed提前过滤掉那些不需要显示的项:
computed: {
items: function() {
return this.list.filter(function (item) {
return item.isShow
})
}
}
异步请求应该放在created还是mounted
created 和 mounted 中发起异步请求是一样的,没有区别。
created跟mounted是同步调用的。ajax是异步的,会经历一次EventLoop才能到ajax的回调。
created 和 mounted 是在同一个 tick 中执行的,而ajax 请求的时间一定会超过一个 tick(一次时间循环称之为一个tick)。所以即便ajax的请求耗时是 0ms, 那么也是在 nextTick 中更新数据到 DOM 中。所以说在不依赖 DOM节点的情况下一点区别都没有。
1.对于页面级组件,当我们需要使用ssr(服务端渲染)的时候,只有created是可用的,所以这个时候请求数据只能用它;
那么什么是服务端渲染(server side render)?顾名思义,服务端渲染就是在浏览器请求页面URL的时候,服务端将我们需要的HTML文本组装好,并返回给浏览器,这个HTML文本被浏览器解析之后,不需要经过 JavaScript 脚本的执行,即可直接构建出希望的 DOM 树并展示到页面中。这个服务端组装HTML的过程,叫做服务端渲染。
2.对于作为子组件被调用的组件里,异步请求应当在mounted里调用,因为这个时候子组件可能需要涉及到对dom的操作;
3.对于页面级组件, 当我们做异步操作时,涉及到要访问dom的操作,我们仍旧只能使用mounted;
4.对于一般情况,created和mounted都是可以的;
v-for 什么时候都不能用index作为key值吗
diff算法默认使用“就地复用”的策略,是一个首尾交叉对比的过程;
用index作为key和不加key是一样的,都采用“就地复用”的策略;
将元素唯一值作为key,可以最大化利用dom节点,提升性能;
“就地复用”的策略,只适用于不依赖子组件状态或临时 DOM 状态 (例如:表单输入值) 的列表渲染输出;
VUE是怎样监听数组变化的
1.对象数据是怎么被监听的
在vue2.x版本中,数据监听是用过Object.defineProperty这个API来实现的,例子:
var obj = {
val: "test"
};
let val = "hello world"
Object.defineProperty(obj, 'val', {
get () {
console.log('获取对象的值');
return val
},
set (newVal) {
console.log(newVal);
console.log('设置对象的新的值为:' + newVal);
val = newVal
}
})
obj.val = val
当我们访问或设置对象的属性的时候,都会触发相对应的函数,然后在这个函数里返回或设置属性的值。我们当然可以在触发函数的时候做我们自己想做的事情,这也就是“劫持”操作。
var data = {
name:'hello',
age:18
}
Object.keys(data).forEach(function(key){
Object.defineProperty(data,key,{
enumerable:true, // 是否能在for...in循环中遍历出来或在Object.keys中列举出来。
configurable:true, // false,不可修改、删除目标属性或修改属性性以下特性
get:function(){
console.log('获取数据');
},
set:function(){
console.log('监听到数据发生了变化');
}
})
});
data.name //控制台会打印出 “获取数据”
data.name = 'world' //控制台会打印出 "监听到数据发生了变化"
2.数组数据是怎么被监听的
上面是对对象的数据进行监听的,我们不能对数组进行数据的“劫持”。那么Vue是怎么做的呢?
import { def } from '../util/index'
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (method) {
// 缓存原来的方法
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})
看来Vue能对数组进行监听的原因是,把数组的方法重写了。总结起来就是这几步:
1.先获取原生 Array 的原型方法,因为拦截后还是需要原生的方法帮我们实现数组的变化
2.对 Array 的原型方法使用 Object.defineProperty 做一些拦截操作
3.把需要被拦截的 Array 类型的数据原型指向改造后原型。
Vue为什么不能检测数组变动
数组在 JS 中常被当作栈,队列,集合等数据结构的实现方式,会有批量的数据以待遍历。并且 runtime 对对象与数组的优化也有所不同。
所以对数组的处理需要特化出来以提高性能。
Vue 中是通过对每个键设置 getter/setter 来实现响应式的,开发者使用数组,目的往往是遍历,此时调用 getter 开销太大了,所以 Vue 不在数组每个键上设置,而是在数组上定义 ob ,并且替换了 push 等等能够影响原数组的原型方法。
$set为啥能检测数组变动
function set (target, key, val) {
//...
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key);
target.splice(key, 1, val);
return val
}
if (key in target && !(key in Object.prototype)) {
target[key] = val;
return val
}
//...
defineReactive$$1(ob.value, key, val);
ob.dep.notify();
return val
}
- 如果target是一个数组且索引有效,就设置length的属性。
- 通过splice方法把value设置到target数组指定位置。
- 设置的时候,vue会拦截到target发生变化,然后把新增的value也变成响应式
- 最后返回value
这就是vue重写数组方法的原因,利用数组这些方法触发使得每一项的value都是响应式的。
父组件可以监听到子组件的生命周期吗?
假如有父组件Index 和 子组件 About,如果父组件监听到子组件挂载到 mounted 时就做一些逻辑处理,可以通过以下写法实现:
<!-- 父组件 -->
<About @mounted="doSomething" />
// 子组件
mounted () {
console.log("子组件触发 mounted");
this.$emit("mounted")
}
以上需要手动通过 $emit 触发父组件的事件,更简单的方式是通过 @hook 来监听:
<!-- 父组件 -->
<About @hook:mounted="doSomething" />
doSomething () {
console.log("父组件监听到子组件的mounted");
this.msg="hello Vue"
}
// 子组件
mounted () {
console.log("子组件触发 mounted");
}
当然 @hook 不仅可以监听 mounted,其他的生命周期 例如 created、updated 都可以监听到。
v-model是如何实现的?语法糖实际是什么?
1.作用在表单元素上
<input v-model="msg">
<!-- v-model等同于下面的代码 -->
<input v-bind:value="msg1"
v-on:input="msg1=$event.target.value" />
2.作用在组件上
v-model 默认会利用名为 value 的 prop和名为 input 的事件
本质上是一个父子组件通信的语法糖,通过 props 和 $emit 实现。
举个栗子:
父组件 price的初始值是100,子组件是一个输入框;输入框的值改变时,更新父组件 price 的值。
<!-- 父组件 -->
<template>
<div>
<h1>Index</h1>
<About v-model="price" />
<span>{{price}}</span>
</div>
</template>
<script>
import About from './About.vue'
export default {
components: { About },
data () {
return {
price: '100'
}
}
}
</script>
<!-- 子组件About -->
<template>
<div>
<input :value="value"
@input="$emit('input',$event.target.value)">
<!--为什么这里把 'input' 作为触发事件的事件名?`input` 在哪定义的?-->
</div>
</template>
<script>
export default {
props: {
value: String,// 为什么这里要定义 value 属性,父组件貌似没有给子组件传递 value 啊?
}
}
</script>
注释里写了两个问题,如果都知道答案,那就是真正掌握了v-model。
上面父组件里使用子组件About 等同于:
<About v-model="price" />
// 等同于:
<About :value="price" @input="price=$event" />
value 和 input 就来自这里。
总结:给组件添加 v-model 属性时,默认会把value 作为组件的属性,把 input作为给组件绑定事件时的事件名
你有对VUE项目进行哪些优化?
详解
1.代码层面的优化
- v-if 和 v-show区分使用场景
- computed 和 watch 区分使用场景
- v-for 遍历必须为 item 添加 key,且避免同时使用 v-if
- 路由懒加载
- 图片资源懒加载:对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载, 等到滚动到可视区域后再去加载。这样对于页面加载性能上会有很大的提升,也提高了用户体验。我们在项目中使用 Vue 的 vue-lazyload 插件,在 vue 文件中将 img 标签的 src 属性直接改为 v-lazy ,从而将图片显示方式更改为懒加载显示:
<img v-lazy="/static/img/1.png">
- 事件的销毁:Vue 组件销毁时,会自动清理它与其它实例的连接,解绑它的全部指令及事件监听器,但是仅限于组件本身的事件。如果在 js 内使用 addEventListene 等方式是不会自动销毁的,我们需要在组件销毁时手动移除这些事件的监听,以免造成内存泄露,如:
created() {
addEventListener('click', this.click, false)
},
beforeDestroy() {
removeEventListener('click', this.click, false)
}
- 长列表性能优化: Vue 会通过 Object.defineProperty 对数据进行劫持,来实现视图响应数据的变化,然而有些时候我们的组件就是纯粹的数据展示,不会有任何改变,我们就不需要 Vue 来劫持我们的数据,在大量数据展示的情况下,这能够很明显的减少组件初始化的时间,那如何禁止 Vue 劫持我们的数据呢?可以通过 Object.freeze 方法来冻结一个对象,一旦被冻结的对象就再也不能被修改了。
this.list = Object.freeze(list);
-
第三方插件的按需引入:我们在项目中经常会需要引入第三方插件,如果我们直接引入整个插件,会导致项目的体积太大,我们可以借助 babel-plugin-component ,然后可以只引入需要的组件,以达到减小项目体积的目的。以下为项目中引入 element-ui 组件库为例:
首先安装babel-plugin-component:
npm install babel-plugin-component -D
然后,将 .babelrc 修改为:
{ "presets": [["es2015", { "modules": false }]], "plugins": [ [ "component", { "libraryName": "element-ui", "styleLibraryName": "theme-chalk" } ] ] }
在 main.js 中引入部分组件:
import Vue from 'vue'; import { Button, Select } from 'element-ui'; Vue.use(Button) Vue.use(Select)
-
优化无限列表性能:如果你的应用存在非常长或者无限滚动的列表,那么需要采用虚拟列表的技术来优化性能,只需要渲染少部分区域的内容,减少重新渲染组件和创建 dom 节点的时间。 你可以参考以下开源项目 vue-virtual-scroll-list (opens new window) 和 vue-virtual-scroller (opens new window)来优化这种无限列表的场景的
-
服务端渲染 SSR or 预渲染:服务端渲染是指 Vue 在客户端将标签渲染成的整个 html 片段的工作在服务端完成,服务端形成的 html 片段直接返回给客户端这个过程就叫做服务端渲染。
1)如果你的项目的 SEO 和 首屏渲染是评价项目的关键指标,那么你的项目就需要服务端渲染来帮助你实现最佳的初始加载性能和 SEO
2)如果你的 Vue 项目只需改善少数营销页面(例如 /, /about, /contact 等)的 SEO,那么你可能需要预渲染,在构建时简单地生成针对特定路由的静态 HTML 文件。 优点是设置预渲染更简单 ,并可以将你的前端作为一个完全静态的站点,具体你可以使用 prerender-spa-plugin (opens new window) 就可以轻松地添加预渲染
2.webpacck 层面的优化
-
压缩图片:一般在vue项目中用webpack打包时,会根据webpack.base.conf.js中url-loader中设置limit大小来对图片处理,对小于limit的图片转化为base64格式,其余的不做操作。所以对有些较大的图片资源,在请求资源的时候,加载会很慢,可以用image-webpack-loader来压缩图片。
-
压缩、合并文件:压缩文件 -> 减少HTTP请求大小,可以减少请求时间;文件合并 -> 减少HTTP请求数量。可以对html、css、js以及图片资源进行压缩处理,现在可以很方便的使用 webpack 实现文件的压缩:
js压缩:UglifyPlugin
CSS压缩:MiniCssExtractPlugin
HTML压缩:HtmlWebpackPlugin
图片压缩:image-webpack-loader -
提取公共代码:Webpack 内置了专门用于提取多个Chunk 中的公共部分的插件 CommonsChunkPlugin,我们在项目中 CommonsChunkPlugin 的配置如下:
// 所有在 package.json 里面依赖的包,都会被打包进 vendor.js 这 个文件中。 new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', minChunks: function(module, count) { return ( module.resource && /\.js$/.test(module.resource) && module.resource.indexOf( path.join(__dirname, '../node_modules') ) === 0 ); } }), // 抽取出代码模块的映射关系 new webpack.optimize.CommonsChunkPlugin({ name: 'manifest', chunks: ['vendor'] }) ```
-
模板预编译
-
提取组件的CSS:当使用单文件组件时,组件内的 CSS 会以 style 标签的方式通过 JavaScript 动态注入。这有一些小小的运行时开销,如果你使用服务端渲染,这会导致一段 “无样式内容闪烁 (fouc) ” 。将所有组件的 CSS 提取到同一个文件可以避免这个问题,也会让 CSS 更好地进行压缩和缓存
-
优化sourceMap
-
Vue 项目的编译优化
基础的 Web 技术的优化
- 开启 gzip 压缩
- 浏览器缓存
- CDN的使用:浏览器从服务器上下载 CSS、js 和图片等文件时都要和服务器连接,而大部分服务器的带宽有限,如果超过限制,网页就半天反应不过来。而 CDN 可以通过不同的域名来加载文件,从而使下载文件的并发连接数大大增加,且CDN 具有更好的可用性,更低的网络延迟和丢包率
- 使用 Chrome Performance 查找性能瓶颈
vue-router 路由钩子函数是什么 执行顺序是什么
路由钩子的执行流程, 钩子函数种类有:全局守卫、路由守卫、组件守卫
完整的导航解析流程:
1.导航被触发。
2.在失活的组件里调用 beforeRouteLeave 守卫。
3.调用全局的 beforeEach 守卫。
4.在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
5.在路由配置里调用 beforeEnter。
6.解析异步路由组件。
7.在被激活的组件里调用 beforeRouteEnter。
8.调用全局的 beforeResolve 守卫 (2.5+)。
9.导航被确认。
10.调用全局的 afterEach 钩子。
11.触发 DOM 更新。
12.调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。
函数式组件优势和原理
函数组件的特点:
1.函数式组件需要在声明组件是指定 functional:true
2.不需要实例化,所以没有this,this通过render函数的第二个参数context来代替
3.没有生命周期钩子函数,不能使用计算属性,watch
4.不能通过$emit 对外暴露事件,调用事件只能通过context.listeners.click的方式调用外部传入的事件
5.因为函数式组件是没有实例化的,所以在外部通过ref去引用组件时,实际引用的是HTMLElement
6.函数式组件的props可以不用显示声明,所以没有在props里面声明的属性都会被自动隐式解析为prop,而普通组件所有未声明的属性都解析到 $attrs里面,并自动挂载到组件根元素上面(可以通过inheritAttrs属性禁止)
优点:
1.由于函数式组件不需要实例化,无状态,没有生命周期,所以渲染性能要好于普通组件
2.函数式组件结构比较简单,代码结构更清晰
使用场景:
1.一个简单的展示组件,作为容器组件使用 比如 router-view 就是一个函数式组件
2.“高阶组件”——用于接收一个组件作为参数,返回一个被包装过的组件
Vue.component('functional',{ // 构造函数产生虚拟节点的
functional:true, // 函数式组件 // data={attrs:{}}
render(h){
return h('div','test')
}
})
const vm = new Vue({
el: '#app'
})
vue要做权限管理该怎么做?如果控制到按钮级别的权限怎么做
一、是什么
权限是对特定资源的访问许可,所谓权限控制,也就是确保用户只能访问到被分配的资源
而前端权限归根结底是请求的发起劝,请求的发起可能由以下两种形式触发:
- 页面加载触发
- 页面上的按钮点击触发
总的来说,所有请求发起都触发自前端路由或视图
所以,我们可以从这两方面入手,对触发权限的源头进行控制,最终要实现的目标是:
- 路由方面:用户登录后只能看到自己有权访问的导航菜单,也只能访问自己有权访问的路由地址,否则将跳转4xx提示页
- 视图方面:用户只能看到自己有权浏览的内容和有权操作的控件
- 最后再加上请求控制作为最后一道防线,路由可能配置失误,按钮可能忘了加权限,这时候请求控制可以用来兜底,越权请求将在前端被拦截
二、如何做
前端权限控制可以分为四个方面:
- 接口权限
- 路由权限
- 菜单权限
- 按钮权限
接口权限
接口权限目前一般采用 jwt 的形式来验证,没通过的话一般返回401,跳转到登录页面重新进行登录
JWT 英文名是 Json Web Token ,是一种用于通信双方之间传递安全信息的简洁的、URL安全的表述性声明规范,经常用在跨域身份验证。
客户端身份经过服务器验证通过后,会生成带有签名的 JSON 对象并将它返回给客户端。客户端在收到这个 JSON 对象后存储起来。
在以后的请求中客户端将 JSON 对象连同请求内容一起发送给服务器,服务器收到请求后通过 JSON 对象标识用户,如果验证不通过则不返回请求的数据。
登录完拿到 token ,将 token 存储起来,通过 axios 请求拦截器进行拦截,每次请求的时候头部携带 token
axios.interceptors.request.use(config => {
config.headers['token'] = cookie.get('token')
return config
})
axios.interceptors.response.use(res=>{},{response}=>{
if (response.data.code === xxx || response.data.code === xxx) { //token过期或者错误
router.push('/login')
}
})
路由权限控制方案
方案一:
菜单与路由分离,菜单由后端返回
前端定义路由信息:
{
name: "login",
path: "/login",
component: () => import("@/pages/Login.vue")
}
name字段都不为空,需要根据此字段与后端返回菜单做关联,后端返回的菜单信息中必须要有name对应的字段,并且做唯一性校验
全局路由守卫里做判断
function hasPermission(router, accessMenu) {
if (whiteList.indexOf(router.path) !== -1) {
return true;
}
let menu = Util.getMenuByName(router.name, accessMenu);
if (menu.name) {
return true;
}
return false;
}
Router.beforeEach(async (to, from, next) => {
if (getToken()) {
let userInfo = store.state.user.userInfo;
if (!userInfo.name) {
try {
await store.dispatch("GetUserInfo")
await store.dispatch('updateAccessMenu')
if (to.path === '/login') {
next({ name: 'home_index' })
} else {
//Util.toDefaultPage([...routers], to.name, router, next);
next({ ...to, replace: true })//菜单权限更新完成,重新进一次当前路由
}
}
catch (e) {
if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入
next()
} else {
next('/login')
}
}
} else {
if (to.path === '/login') {
next({ name: 'home_index' })
} else {
if (hasPermission(to, store.getters.accessMenu)) {
Util.toDefaultPage(store.getters.accessMenu,to, routes, next);
} else {
next({ path: '/403',replace:true })
}
}
}
} else {
if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入
next()
} else {
next('/login')
}
}
let menu = Util.getMenuByName(to.name, store.getters.accessMenu);
Util.title(menu.title);
});
Router.afterEach((to) => {
window.scrollTo(0, 0);
});
每次路由跳转的时候都要判断权限,这里的判断也很简单,因为菜单的name与路由的name是一一对应的,而后端返回的菜单就已经是经过权限过滤的
如果根据路由name找不到对应的菜单,就表示用户有没权限访问
如果路由很多,可以在应用初始化的时候,只挂载不需要权限控制的路由。取得后端返回的菜单后,根据菜单与路由的对应关系,筛选出可访问的路由,通过addRoutes动态挂载
这种方式的缺点:
- 菜单需要与路由做一一对应,前端添加了新功能,需要通过菜单管理功能添加新的菜单,如果菜单配置的不对会导致应用不能正常使用
- 全局路由守卫里,每次路由跳转都要做判断
方案二:
菜单和路由都由后端返回
前端统一定义路由组件:
const Home = () => import("../pages/Home.vue");
const UserInfo = () => import("../pages/UserInfo.vue");
export default {
home: Home,
userInfo: UserInfo
};
后端路由组件返回以下格式:
[
{
name: "home",
path: "/",
component: "home"
},
{
name: "home",
path: "/userinfo",
component: "userInfo"
}
]
在将后端返回路由通过addRoutes动态挂载之间,需要将数据处理一下,将component字段换为真正的组件
如果有嵌套路由,后端功能设计的时候,要注意添加相应的字段,前端拿到数据也要做相应的处理
这种方法也会存在缺点:
- 全局路由守卫里,每次路由跳转都要做判断
- 前后端的配合要求更高
按钮权限
方案一:
按钮权限也可以用v-if判断
但是如果页面过多,每个页面页面都要获取用户权限role和路由表里的meta.btnPermissions,然后再做判断
这种方式就不展开举例了
方案二:
通过自定义指令进行按钮权限的判断
首先配置路由:
{
path: '/permission',
component: Layout,
name: '权限测试',
meta: {
btnPermissions: ['admin', 'supper', 'normal']
},
//页面需要的权限
children: [{
path: 'supper',
component: _import('system/supper'),
name: '权限测试页',
meta: {
btnPermissions: ['admin', 'supper']
} //页面需要的权限
},
{
path: 'normal',
component: _import('system/normal'),
name: '权限测试页',
meta: {
btnPermissions: ['admin']
} //页面需要的权限
}]
}
自定义权限鉴定指令:
import Vue from 'vue'
/**权限指令**/
const has = Vue.directive('has', {
bind: function (el, binding, vnode) {
// 获取页面按钮权限
let btnPermissionsArr = [];
if(binding.value){
// 如果指令传值,获取指令参数,根据指令参数和当前登录人按钮权限做比较。
btnPermissionsArr = Array.of(binding.value);
}else{
// 否则获取路由中的参数,根据路由的btnPermissionsArr和当前登录人按钮权限做比较。
btnPermissionsArr = vnode.context.$route.meta.btnPermissions;
}
if (!Vue.prototype.$_has(btnPermissionsArr)) {
el.parentNode.removeChild(el);
}
}
});
// 权限检查方法
Vue.prototype.$_has = function (value) {
let isExist = false;
// 获取用户按钮权限
let btnPermissionsStr = sessionStorage.getItem("btnPermissions");
if (btnPermissionsStr == undefined || btnPermissionsStr == null) {
return false;
}
if (value.indexOf(btnPermissionsStr) > -1) {
isExist = true;
}
return isExist;
};
export {has}
在使用的按钮中只需要引用v-has指令:
<el-button @click='editClick' type="primary" v-has>编辑</el-button>
小结
关于权限如何选择哪种合适的方案,可以根据自己项目的方案项目,如考虑路由与菜单是否分离
权限需要前后端结合,前端尽可能的去控制,更多的需要后台判断
Vue项目中有封装过axios吗?主要是封装哪方面的?
一、axios是什么
axios 是一个轻量的 HTTP客户端
基于 XMLHttpRequest 服务来执行 HTTP 请求,支持丰富的配置,支持 Promise,支持浏览器端和 Node.js端。自Vue2.0起,尤大宣布取消对 vue-resource 的官方推荐,转而推荐 axios。现在 axios 已经成为大部分 Vue开发者的首选
特性
- 从浏览器中创建 XMLHttpRequests
- 从 node.js 创建 http请求
- 支持 Promise API
- 拦截请求和响应
- 转换请求数据和响应数据
- 取消请求
- 自动转换JSON 数据
- 客户端支持防御XSRF
二、为什么要封装
axios 的 API 很友好,你完全可以很轻松地在项目中直接使用。
不过随着项目规模增大,如果每发起一次HTTP请求,就要把这些比如设置超时时间、设置请求头、根据项目环境判断使用哪个请求地址、错误处理等等操作,都需要写一遍
这种重复劳动不仅浪费时间,而且让代码变得冗余不堪,难以维护。为了提高我们的代码质量,我们应该在项目中二次封装一下 axios 再使用
举个例子:
axios('http://localhost:3000/data', {
// 配置代码
method: 'GET',
timeout: 1000,
withCredentials: true,
headers: {
'Content-Type': 'application/json',
Authorization: 'xxx',
},
transformRequest: [function (data, headers) {
return data;
}],
// 其他请求配置...
})
.then((data) => {
// todo: 真正业务逻辑代码
console.log(data);
}, (err) => {
// 错误处理代码
if (err.response.status === 401) {
// handle authorization error
}
if (err.response.status === 403) {
// handle server forbidden error
}
// 其他错误处理.....
console.log(err);
});
如果每个页面都发送类似的请求,都要写一堆的配置与错误处理,就显得过于繁琐了
这时候我们就需要对axios进行二次封装,让使用更为便利
三、如何封装
- 封装的同时,你需要和 后端协商好一些约定,请求头,状态码,请求超时时间…
- 设置接口请求前缀:根据开发、测试、生产环境的不同,前缀需要加以区分
- 请求头 : 来实现一些具体的业务,必须携带一些参数才可以请求(例如:会员业务)
- 状态码: 根据接口返回的不同status , 来执行不同的业务,这块需要和后端约定好
- 请求方法:根据get、post等方法进行一个再次封装,使用起来更为方便
- 请求拦截器: 根据请求的请求头设定,来决定哪些请求可以访问
- 响应拦截器: 这块就是根据 后端`返回来的状态码判定执行不同业务
设置接口请求前缀
利用node环境变量来作判断,用来区分开发、测试、生产环境:
if (process.env.NODE_ENV === 'development') {
axios.defaults.baseURL = 'http://dev.xxx.com'
} else if (process.env.NODE_ENV === 'production') {
axios.defaults.baseURL = 'http://prod.xxx.com'
}
在本地调试的时候,还需要在vue.config.js文件中配置devServer实现代理转发,从而实现跨域:
devServer: {
proxy: {
'/proxyApi': {
target: 'http://dev.xxx.com',
changeOrigin: true,
pathRewrite: {
'/proxyApi': ''
}
}
}
}
设置请求头与超时时间
大部分情况下,请求头都是固定的,只有少部分情况下,会需要一些特殊的请求头,这里将普适性的请求头作为基础配置。当需要特殊请求头时,将特殊请求头作为参数传入,覆盖基础配置
const service = axios.create({
...
timeout: 30000, // 请求 30s 超时
headers: {
get: {
'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8'
// 在开发中,一般还需要单点登录或者其他功能的通用请求头,可以一并配置进来
},
post: {
'Content-Type': 'application/json;charset=utf-8'
// 在开发中,一般还需要单点登录或者其他功能的通用请求头,可以一并配置进来
}
},
})
封装请求方法
先引入封装好的方法,在要调用的接口重新封装成一个方法暴露出去:
// get 请求
export function httpGet({
url,
params = {}
}) {
return new Promise((resolve, reject) => {
axios.get(url, {
params
}).then((res) => {
resolve(res.data)
}).catch(err => {
reject(err)
})
})
}
// post
// post请求
export function httpPost({
url,
data = {},
params = {}
}) {
return new Promise((resolve, reject) => {
axios({
url,
method: 'post',
transformRequest: [function (data) {
let ret = ''
for (let it in data) {
ret += encodeURIComponent(it) + '=' + encodeURIComponent(data[it]) + '&'
}
return ret
}],
// 发送的数据
data,
// url参数
params
}).then(res => {
resolve(res.data)
})
})
}
把封装的方法放在一个api.js文件中:
import { httpGet, httpPost } from './http'
export const getorglist = (params = {}) => httpGet({ url: 'apps/api/org/list', params })
页面中就能直接调用:
// .vue
import { getorglist } from '@/assets/js/api'
getorglist({ id: 200 }).then(res => {
console.log(res)
})
这样可以把api统一管理起来,以后维护修改只需要在api.js文件操作即可
请求拦截器
请求拦截器可以在每个请求里加上token,做了统一处理后维护起来也方便:
// 请求拦截器
axios.interceptors.request.use(
config => {
// 每次发送请求之前判断是否存在token
// 如果存在,则统一在http请求的header都加上token,这样后台根据token判断你的登录情况,此处token一般是用户完成登录后储存到localstorage里的
token && (config.headers.Authorization = token)
return config
},
error => {
return Promise.error(error)
})
响应拦截器
响应拦截器可以在接收到响应后先做一层操作,如根据状态码判断登录状态、授权:
// 响应拦截器
axios.interceptors.response.use(response => {
// 如果返回的状态码为200,说明接口请求成功,可以正常拿到数据
// 否则的话抛出错误
if (response.status === 200) {
if (response.data.code === 511) {
// 未授权调取授权接口
} else if (response.data.code === 510) {
// 未登录跳转登录页
} else {
return Promise.resolve(response)
}
} else {
return Promise.reject(response)
}
}, error => {
// 我们可以在这里对异常状态作统一处理
if (error.response.status) {
// 处理请求失败的情况
// 对不同返回码对相应处理
return Promise.reject(error.response)
}
})
小结
- 封装是编程中很有意义的手段,简单的axios封装,就可以让我们可以领略到它的魅力
- 封装 axios 没有一个绝对的标准,只要你的封装可以满足你的项目需求,并且用起来方便,那就是一个好的封装方案
Vue自定义指令
在 Vue2.0 中,代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。
一般需要对DOM元素进行底层操作时使用,尽量只用来操作 DOM展示,不修改内部的值。当使用自定义指令直接修改 value 值时绑定v-model的值也不会同步更新;如必须修改可以在自定义指令中使用keydown事件,在vue组件中使用 change事件,回调中修改vue数据;
(1)自定义指令基本内容
-
全局定义:Vue.directive(“focus”,{})
-
局部定义:directives:{focus:{}}
-
钩子函数:指令定义对象提供钩子函数:
bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置
inSerted:被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被插入文档中)
update:所在组件的VNode更新时调用,但是可能发生在其子VNode更新之前调用。指令的值可能发生了改变,也可能没有。但是可以通过比较更新前后的值来忽略不必要的模板更新
ComponentUpdate:指令所在组件的 VNode及其子VNode全部更新后调用
unbind:只调用一次,指令与元素解绑时调用 -
钩子函数参数
el:绑定元素
bing: 指令核心对象,描述指令全部信息属性
name
value
oldValue
expression
arg
modifers
vnode 虚拟节点
oldVnode:上一个虚拟节点(更新钩子函数中才有用)
(2)使用场景
- 普通DOM元素进行底层操作的时候,可以使用自定义指令
- 自定义指令是用来操作DOM的。尽管Vue推崇数据驱动视图的理念,但并非所有情况都适合数据驱动。自定义指令就是一种有效的补充和扩展,不仅可用于定义任何的DOM操作,并且是可复用的。