关于vue.js:三年经验前端vue面试记录

4次阅读

共计 38766 个字符,预计需要花费 97 分钟才能阅读完成。

router-link 和 router-view 是如何起作用的

剖析

vue-router中两个重要组件 router-linkrouter-view,别离起到导航作用和内容渲染作用,然而答复如何失效还真有肯定难度

答复范例

  1. vue-router中两个重要组件 router-linkrouter-view,别离起到路由导航作用和组件内容渲染作用
  2. 应用中 router-link 默认生成一个 a 标签,设置 to 属性定义跳转 path。实际上也能够通过custom 和插槽自定义最终的展示模式。router-view是要显示组件的占位组件,能够嵌套,对应路由配置的嵌套关系,配合 name 能够显示具名组件,起到更强的布局作用。
  3. router-link组件外部依据 custom 属性判断如何渲染最终生成节点,外部提供导航办法 navigate,用户点击之后理论调用的是该办法,此办法最终会批改响应式的路由变量,而后从新去routes 匹配出数组后果,router-view则依据其所处深度 deep 在匹配数组后果中找到对应的路由并获取组件,最终将其渲染进去。

vuex 是什么?怎么应用?哪种性能场景应用它?

Vuex 是一个专为 Vue.js 利用程序开发的状态管理模式。vuex 就是一个仓库,仓库里放了很多对象。其中 state 就是数据源寄存地,对应于个别 vue 对象外面的 data 外面寄存的数据是响应式的,vue 组件从 store 读取数据,若是 store 中的数据产生扭转,依赖这相数据的组件也会产生更新它通过 mapState 把全局的 stategetters 映射到以后组件的 computed 计算属性

  • vuex 个别用于中大型 web 单页利用中对利用的状态进行治理,对于一些组件间关系较为简单的小型利用,应用 vuex 的必要性不是很大,因为齐全能够用组件 prop 属性或者事件来实现父子组件之间的通信,vuex 更多地用于解决跨组件通信以及作为数据中心集中式存储数据。
  • 应用 Vuex 解决非父子组件之间通信问题 vuex 是通过将 state 作为数据中心、各个组件共享 state 实现跨组件通信的,此时的数据齐全独立于组件,因而将组件间共享的数据置于 State 中能无效解决多层级组件嵌套的跨组件通信问题

vuexState 在单页利用的开发中自身具备一个“数据库”的作用,能够将组件中用到的数据存储在 State 中,并在 Action 中封装数据读写的逻辑。这时候存在一个问题,个别什么样的数据会放在 State 中呢?目前次要有两种数据会应用 vuex 进行治理:

  • 组件之间全局共享的数据
  • 通过后端异步申请的数据

包含以下几个模块

  • stateVuex 应用繁多状态树, 即每个利用将仅仅蕴含一个store 实例。外面寄存的数据是响应式的,vue 组件从 store 读取数据,若是 store 中的数据产生扭转,依赖这相数据的组件也会产生更新。它通过 mapState 把全局的 stategetters 映射到以后组件的 computed 计算属性
  • mutations:更改 Vuexstore中的状态的惟一办法是提交mutation
  • gettersgetter 能够对 state 进行计算操作,它就是 store 的计算属性尽管在组件内也能够做计算属性,然而 getters 能够在多给件之间复用如果一个状态只在一个组件内应用,是能够不必 getters
  • actionaction 相似于 muation, 不同在于:action 提交的是 mutation, 而不是间接变更状态action 能够蕴含任意异步操作
  • modules:面对简单的应用程序,当治理的状态比拟多时;咱们须要将 vuexstore对象宰割成模块(modules)

modules:我的项目特地简单的时候,能够让每一个模块领有本人的statemutationactiongetters,使得构造十分清晰,方便管理

答复范例

思路

  • 给定义
  • 必要性论述
  • 何时应用
  • 拓展:一些集体思考、实践经验等

答复范例

  1. Vuex 是一个专为 Vue.js 利用开发的 状态管理模式 + 库。它采纳集中式存储,治理利用的所有组件的状态,并以相应的规定保障状态以一种可预测的形式发生变化。
  2. 咱们期待以一种简略的“单向数据流”的形式治理利用,即 状态 -> 视图 -> 操作单向循环 的形式。但当咱们的利用遇到多个组件共享状态时,比方:多个视图依赖于同一状态或者来自不同视图的行为须要变更同一状态。此时单向数据流的简洁性很容易被毁坏。因而,咱们有必要把组件的共享状态抽取进去,以一个全局单例模式治理。通过定义和隔离状态治理中的各种概念并通过强制规定维持视图和状态间的独立性,咱们的代码将会变得更结构化且易保护。这是 vuex 存在的必要性,它和 react 生态中的 redux 之类是一个概念
  3. Vuex 解决状态治理的同时引入了不少概念:例如 statemutationaction 等,是否须要引入还须要依据利用的理论状况掂量一下:如果不打算开发大型单页利用,应用 Vuex 反而是繁琐冗余的,一个简略的 store 模式就足够了。然而,如果要构建一个中大型单页利用,Vuex 根本是标配。
  4. 我在应用 vuex 过程中感触到一些等

可能的诘问

  1. vuex有什么毛病吗?你在开发过程中有遇到什么问题吗?
  2. 刷新浏览器,vuex中的 state 会从新变为初始状态。解决方案 - 插件 vuex-persistedstate
  3. actionmutation 的区别是什么?为什么要辨别它们?
  • action中解决异步,mutation不能够
  • mutation做原子操作
  • action能够整合多个 mutation 的汇合
  • mutation 是同步更新数据(外部会进行是否为异步形式更新数据的检测) $watch 严格模式下会报错
  • action 异步操作,能够获取数据后调佣 mutation 提交最终数据
  • 流程程序:“相应视图—> 批改 State”拆分成两局部,视图触发ActionAction 再触发Mutation`。
  • 基于流程程序,二者表演不同的角色:Mutation:专一于批改 State,实践上是批改State 的惟一路径。Action:业务代码、异步申请
  • 角色不同,二者有不同的限度:Mutation:必须同步执行。Action:能够异步,但不能间接操作State

三、分类

slot能够分来以下三种:

  • 默认插槽
  • 具名插槽
  • 作用域插槽

1. 默认插槽

子组件用 <slot> 标签来确定渲染的地位,标签外面能够放 DOM 构造,当父组件应用的时候没有往插槽传入内容,标签内 DOM 构造就会显示在页面

父组件在应用的时候,间接在子组件的标签内写入内容即可

子组件Child.vue

<template>
    <slot>
      <p> 插槽后备的内容 </p>
    </slot>
</template>

父组件

<Child>
  <div> 默认插槽 </div>  
</Child>

2. 具名插槽

子组件用 name 属性来示意插槽的名字,不传为默认插槽

父组件中在应用时在默认插槽的根底上加上 slot 属性,值为子组件插槽 name 属性值

子组件Child.vue

<template>
    <slot> 插槽后备的内容 </slot>
  <slot name="content"> 插槽后备的内容 </slot>
</template>

父组件

<child>
    <template v-slot:default> 具名插槽 </template>
    <!-- 具名插槽⽤插槽名做参数 -->
    <template v-slot:content> 内容...</template>
</child>

3. 作用域插槽

子组件在作用域上绑定属性来将子组件的信息传给父组件应用,这些属性会被挂在父组件 v-slot 承受的对象上

父组件中在应用时通过v-slot:(简写:#)获取子组件的信息,在内容中应用

子组件Child.vue

<template> 
  <slot name="footer" testProps="子组件的值">
          <h3> 没传 footer 插槽 </h3>
    </slot>
</template>

父组件

<child> 
    <!-- 把 v -slot 的值指定为作⽤域高低⽂对象 -->
    <template v-slot:default="slotProps">
      来⾃⼦组件数据:{{slotProps.testProps}}
    </template>
    <template #default="slotProps">
      来⾃⼦组件数据:{{slotProps.testProps}}
    </template>
</child>

小结:

  • v-slot属性只能在 <template> 上应用,但在只有默认插槽时能够在组件标签上应用
  • 默认插槽名为default,能够省略 default 间接写v-slot
  • 缩写为 # 时不能不写参数,写成#default
  • 能够通过解构获取 v-slot={user},还能够重命名v-slot="{user: newName}" 和定义默认值v-slot="{user =' 默认值 '}"

四、原理剖析

slot实质上是返回 VNode 的函数,个别状况下,Vue中的组件要渲染到页面上须要通过 template -> render function -> VNode -> DOM 过程,这里看看slot 如何实现:

编写一个 buttonCounter 组件,应用匿名插槽

Vue.component('button-counter', {template: '<div> <slot> 我是默认内容 </slot></div>'})

应用该组件

new Vue({
    el: '#app',
    template: '<button-counter><span> 我是 slot 传入内容 </span></button-counter>',
    components:{buttonCounter}
})

获取 buttonCounter 组件渲染函数

(function anonymous() {with(this){return _c('div',[_t("default",[_v("我是默认内容")])],2)}
})

_v示意穿件一般文本节点,_t示意渲染插槽的函数

渲染插槽函数renderSlot(做了简化)

function renderSlot (
  name,
  fallback,
  props,
  bindObject
) {
  // 失去渲染插槽内容的函数    
  var scopedSlotFn = this.$scopedSlots[name];
  var nodes;
  // 如果存在插槽渲染函数,则执行插槽渲染函数,生成 nodes 节点返回
  // 否则应用默认值
  nodes = scopedSlotFn(props) || fallback;
  return nodes;
}

name属性示意定义插槽的名字,默认值为 defaultfallback 示意子组件中的 slot 节点的默认值

对于 this.$scopredSlots 是什么,咱们能够先看看vm.slot

function initRender (vm) {
  ...
  vm.$slots = resolveSlots(options._renderChildren, renderContext);
  ...
}

resolveSlots函数会对 children 节点做归类和过滤解决,返回slots

function resolveSlots (
    children,
    context
  ) {if (!children || !children.length) {return {}
    }
    var slots = {};
    for (var i = 0, l = children.length; i < l; i++) {var child = children[i];
      var data = child.data;
      // remove slot attribute if the node is resolved as a Vue slot node
      if (data && data.attrs && data.attrs.slot) {delete data.attrs.slot;}
      // named slots should only be respected if the vnode was rendered in the
      // same context.
      if ((child.context === context || child.fnContext === context) &&
        data && data.slot != null
      ) {// 如果 slot 存在(slot="header") 则拿对应的值作为 key
        var name = data.slot;
        var slot = (slots[name] || (slots[name] = []));
        // 如果是 tempalte 元素 则把 template 的 children 增加进数组中,这也就是为什么你写的 template 标签并不会渲染成另一个标签到页面
        if (child.tag === 'template') {slot.push.apply(slot, child.children || []);
        } else {slot.push(child);
        }
      } else {
        // 如果没有就默认是 default
        (slots.default || (slots.default = [])).push(child);
      }
    }
    // ignore slots that contains only whitespace
    for (var name$1 in slots) {if (slots[name$1].every(isWhitespace)) {delete slots[name$1];
      }
    }
    return slots
}

_render渲染函数通过 normalizeScopedSlots 失去vm.$scopedSlots

vm.$scopedSlots = normalizeScopedSlots(
  _parentVnode.data.scopedSlots,
  vm.$slots,
  vm.$scopedSlots
);

作用域插槽中父组件可能失去子组件的值是因为在 renderSlot 的时候执行会传入 props,也就是上述_t 第三个参数,父组件则可能失去子组件传递过去的值

Vue-Router 的懒加载如何实现

非懒加载:

import List from '@/components/list.vue'
const router = new VueRouter({
  routes: [{ path: '/list', component: List}
  ]
})

(1)计划一(罕用):应用箭头函数 +import 动静加载

const List = () => import('@/components/list.vue')
const router = new VueRouter({
  routes: [{ path: '/list', component: List}
  ]
})

(2)计划二:应用箭头函数 +require 动静加载

const router = new Router({
  routes: [
   {
     path: '/list',
     component: resolve => require(['@/components/list'], resolve)
   }
  ]
})

(3)计划三:应用 webpack 的 require.ensure 技术,也能够实现按需加载。这种状况下,多个路由指定雷同的 chunkName,会合并打包成一个 js 文件。

// r 就是 resolve
const List = r => require.ensure([], () => r(require('@/components/list')), 'list');
// 路由也是失常的写法  这种是官网举荐的写的 按模块划分懒加载 
const router = new Router({
  routes: [
  {
    path: '/list',
    component: List,
    name: 'list'
  }
 ]
}))

Vue Ref 的作用

  • 获取 dom 元素this.$refs.box
  • 获取子组件中的datathis.$refs.box.msg
  • 调用子组件中的办法this.$refs.box.open()

vue 要做权限治理该怎么做?如果管制到按钮级别的权限怎么做

一、是什么

权限是对特定资源的拜访许可,所谓权限管制,也就是确保用户只能拜访到被调配的资源

而前端权限归根结底是申请的发动权,申请的发动可能有上面两种模式触发

  • 页面加载触发
  • 页面上的按钮点击触发

总的来说,所有的申请发动都触发自前端路由或视图

所以咱们能够从这两方面动手,对触发权限的源头进行管制,最终要实现的指标是:

  • 路由方面,用户登录后只能看到本人有权拜访的导航菜单,也只能拜访本人有权拜访的路由地址,否则将跳转 4xx 提醒页
  • 视图方面,用户只能看到本人有权浏览的内容和有权操作的控件
  • 最初再加上申请管制作为最初一道防线,路由可能配置失误,按钮可能忘了加权限,这种时候申请管制能够用来兜底,越权申请将在前端被拦挡

二、如何做

前端权限管制能够分为四个方面:

  • 接口权限
  • 按钮权限
  • 菜单权限
  • 路由权限

接口权限

接口权限目前个别采纳 jwt 的模式来验证,没有通过的话个别返回401,跳转到登录页面从新进行登录

登录完拿到 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 === 40099 || response.data.code === 40098) { //token 过期或者谬误
        router.push('/login')
    }
})

路由权限管制

计划一

初始化即挂载全副路由,并且在路由上标记相应的权限信息,每次路由跳转前做校验

const routerMap = [
  {
    path: '/permission',
    component: Layout,
    redirect: '/permission/index',
    alwaysShow: true, // will always show the root menu
    meta: {
      title: 'permission',
      icon: 'lock',
      roles: ['admin', 'editor'] // you can set roles in root nav
    },
    children: [{
      path: 'page',
      component: () => import('@/views/permission/page'),
      name: 'pagePermission',
      meta: {
        title: 'pagePermission',
        roles: ['admin'] // or you can only set roles in sub nav
      }
    }, {
      path: 'directive',
      component: () => import('@/views/permission/directive'),
      name: 'directivePermission',
      meta: {
        title: 'directivePermission'
        // if do not set roles, means: this page does not require permission
      }
    }]
  }]

这种形式存在以下四种毛病:

  • 加载所有的路由,如果路由很多,而用户并不是所有的路由都有权限拜访,对性能会有影响。
  • 全局路由守卫里,每次路由跳转都要做权限判断。
  • 菜单信息写死在前端,要改个显示文字或权限信息,须要从新编译
  • 菜单跟路由耦合在一起,定义路由的时候还有增加菜单显示题目,图标之类的信息,而且路由不肯定作为菜单显示,还要多加字段进行标识

计划二

初始化的时候先挂载不须要权限管制的路由,比方登录页,404 等谬误页。如果用户通过 URL 进行强制拜访,则会间接进入 404,相当于从源头上做了管制

登录后,获取用户的权限信息,而后筛选有权限拜访的路由,在全局路由守卫里进行调用 addRoutes 增加路由

import router from './router'
import store from './store'
import {Message} from 'element-ui'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css'// progress bar style
import {getToken} from '@/utils/auth' // getToken from cookie

NProgress.configure({showSpinner: false})// NProgress Configuration

// permission judge function
function hasPermission(roles, permissionRoles) {if (roles.indexOf('admin') >= 0) return true // admin permission passed directly
  if (!permissionRoles) return true
  return roles.some(role => permissionRoles.indexOf(role) >= 0)
}

const whiteList = ['/login', '/authredirect']// no redirect whitelist

router.beforeEach((to, from, next) => {NProgress.start() // start progress bar
  if (getToken()) { // determine if there has token
    /* has token*/
    if (to.path === '/login') {next({ path: '/'})
      NProgress.done() // if current page is dashboard will not trigger    afterEach hook, so manually handle it} else {if (store.getters.roles.length === 0) { // 判断以后用户是否已拉取完 user_info 信息
        store.dispatch('GetUserInfo').then(res => { // 拉取 user_info
          const roles = res.data.roles // note: roles must be a array! such as: ['editor','develop']
          store.dispatch('GenerateRoutes', { roles}).then(() => { // 依据 roles 权限生成可拜访的路由表
            router.addRoutes(store.getters.addRouters) // 动静增加可拜访路由表
            next({...to, replace: true}) // hack 办法 确保 addRoutes 已实现 ,set the replace: true so the navigation will not leave a history record
          })
        }).catch((err) => {store.dispatch('FedLogOut').then(() => {Message.error(err || 'Verification failed, please login again')
            next({path: '/'})
          })
        })
      } else {// 没有动静扭转权限的需要可间接 next() 删除下方权限判断 ↓
        if (hasPermission(store.getters.roles, to.meta.roles)) {next()//
        } else {next({ path: '/401', replace: true, query: { noGoBack: true}})
        }
        // 可删 ↑
      }
    }
  } else {
    /* has no token*/
    if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,间接进入
      next()} else {next('/login') // 否则全副重定向到登录页
      NProgress.done() // if current page is login will not trigger afterEach hook, so manually handle it}
  }
})

router.afterEach(() => {NProgress.done() // finish progress bar
})

按需挂载,路由就须要晓得用户的路由权限,也就是在用户登录进来的时候就要晓得以后用户领有哪些路由权限

这种形式也存在了以下的毛病:

  • 全局路由守卫里,每次路由跳转都要做判断
  • 菜单信息写死在前端,要改个显示文字或权限信息,须要从新编译
  • 菜单跟路由耦合在一起,定义路由的时候还有增加菜单显示题目,图标之类的信息,而且路由不肯定作为菜单显示,还要多加字段进行标识

菜单权限

菜单权限能够了解成将页面与理由进行解耦

计划一

菜单与路由拆散,菜单由后端返回

前端定义路由信息

{
    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

根本应用

装置

// 我的项目中装置
npm install axios --S
// cdn 引入
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>

导入

import axios from 'axios'

发送申请

axios({        
  url:'xxx',    // 设置申请的地址
  method:"GET", // 设置申请办法
  params:{      // get 申请应用 params 进行参数凭借, 如果是 post 申请用 data
    type: '',
    page: 1
  }
}).then(res => {  
  // res 为后端返回的数据
  console.log(res);   
})

并发申请axios.all([])

function getUserAccount() {return axios.get('/user/12345');
}

function getUserPermissions() {return axios.get('/user/12345/permissions');
}

axios.all([getUserAccount(), getUserPermissions()])
    .then(axios.spread(function (res1, res2) { 
    // res1 第一个申请的返回的内容,res2 第二个申请返回的内容
    // 两个申请都执行实现才会执行
}));

二、为什么要封装

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,来执行不同的业务,这块须要和后端约定好
  • 申请办法:依据 getpost 等办法进行一个再次封装,应用起来更为不便
  • 申请拦截器: 依据申请的申请头设定,来决定哪些申请能够拜访
  • 响应拦截器:这块就是依据 后端 ` 返回来的状态码断定执行不同业务

设置接口申请前缀

利用 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 面试题具体解答

v-on 能够监听多个办法吗?

能够监听多个办法

<input type="text" :value="name" @input="onInput" @focus="onFocus" @blur="onBlur" />

v-on 罕用修饰符

  • .stop 该修饰符将阻止事件向上冒泡。同理于调用 event.stopPropagation() 办法
  • .prevent 该修饰符会阻止以后事件的默认行为。同理于调用 event.preventDefault() 办法
  • .self 该指令只当事件是从事件绑定的元素自身触发时才触发回调
  • .once 该修饰符示意绑定的事件只会被触发一次

Vue 我的项目性能优化 - 具体

Vue 框架通过数据双向绑定和虚构 DOM 技术,帮咱们解决了前端开发中最脏最累的 DOM 操作局部,咱们不再须要去思考如何操作 DOM 以及如何最高效地操作 DOM;但 Vue 我的项目中依然存在我的项目首屏优化、Webpack 编译配置优化等问题,所以咱们依然须要去关注 Vue 我的项目性能方面的优化,使我的项目具备更高效的性能、更好的用户体验

代码层面的优化

1. v-if 和 v-show 辨别应用场景

  • v-if 是 真正 的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块
  • v-show 就简略得多,不论初始条件是什么,元素总是会被渲染,并且只是简略地基于 CSS displaynone/block 属性进行切换。
  • 所以,v-if 实用于在运行时很少扭转条件,不须要频繁切换条件的场景;v-show 则实用于须要十分频繁切换条件的场景

2. computed 和 watch 辨别应用场景

  • computed:是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值产生扭转,下一次获取 computed 的值时才会从新计算 computed 的值;
  • watch:更多的是「察看」的作用,相似于某些数据的监听回调,每当监听的数据变动时都会执行回调进行后续操作

使用场景:

  • 当咱们须要进行数值计算,并且依赖于其它数据时,应该应用 computed,因为能够利用 computed 的缓存个性,防止每次获取值时,都要从新计算;
  • 当咱们须要在数据变动时执行异步或开销较大的操作时,应该应用 watch,应用 watch 选项容许咱们执行异步操作 (拜访一个 API ),限度咱们执行该操作的频率,并在咱们失去最终后果前,设置中间状态。这些都是计算属性无奈做到的

3. v-for 遍历必须为 item 增加 key,且防止同时应用 v-if

  • v-for 遍历必须为 item 增加 key

    • 在列表数据进行遍历渲染时,须要为每一项 item 设置惟一 key 值,不便 Vue.js 外部机制精准找到该条列表数据。当 state 更新时,新的状态值和旧的状态值比照,较快地定位到 diff
  • v-for 遍历防止同时应用 v-if

    • vue2.xv-forv-if 优先级高,如果每一次都须要遍历整个数组,将会影响速度,尤其是当之须要渲染很小一部分的时候,必要状况下应该替换成 computed 属性

举荐:

<ul>
  <li
    v-for="user in activeUsers"
    :key="user.id">
    {{user.name}}
  </li>
</ul>
computed: {activeUsers: function () {return this.users.filter(function (user) {return user.isActive})
  }
}

不举荐:

<ul>
  <li
    v-for="user in users"
    v-if="user.isActive"
    :key="user.id">
    {{user.name}}
  </li>
</ul>

4. 长列表性能优化

Vue 会通过 Object.defineProperty 对数据进行劫持,来实现视图响应数据的变动,然而有些时候咱们的组件就是纯正的数据展现,不会有任何扭转,咱们就不须要 Vue 来劫持咱们的数据,在大量数据展现的状况下,这可能很显著的缩小组件初始化的工夫,那如何禁止 Vue 劫持咱们的数据呢?能够通过 Object.freeze 办法来解冻一个对象,一旦被解冻的对象就再也不能被批改了

export default {data: () => ({users: {}
  }),
  async created() {const users = await axios.get("/api/users");
    this.users = Object.freeze(users);
  }
};

5. 事件的销毁

Vue 组件销毁时,会主动清理它与其它实例的连贯,解绑它的全副指令及事件监听器,然而仅限于组件自身的事件。如果在 js 内应用 addEventListener 等形式是不会主动销毁的,咱们须要在组件销毁时手动移除这些事件的监听,免得造成内存泄露,如:

created() {addEventListener('click', this.click, false)
},
beforeDestroy() {removeEventListener('click', this.click, false)
}

6. 图片资源懒加载

对于图片过多的页面,为了减速页面加载速度,所以很多时候咱们须要将页面内未呈现在可视区域内的图片先不做加载,等到滚动到可视区域后再去加载。这样对于页面加载性能上会有很大的晋升,也进步了用户体验。咱们在我的项目中应用 Vuevue-lazyload 插件

npm install vue-lazyload --save-dev

在入口文件 man.js 中引入并应用

import VueLazyload from 'vue-lazyload'

Vue.use(VueLazyload)

// 或者增加自定义选项
Vue.use(VueLazyload, {
  preLoad: 1.3,
  error: 'dist/error.png',
  loading: 'dist/loading.gif',
  attempt: 1
})

vue 文件中将 img 标签的 src 属性间接改为 v-lazy,从而将图片显示方式更改为懒加载显示

<img v-lazy="/static/img/1.png">

以上为 vue-lazyload 插件的简略应用,如果要看插件的更多参数选项,能够查看 vue-lazyload 的 github 地址(opens new window)

7. 路由懒加载

Vue 是单页面利用,可能会有很多的路由引入,这样应用 webpcak 打包后的文件很大,当进入首页时,加载的资源过多,页面会呈现白屏的状况,不利于用户体验。如果咱们能把不同路由对应的组件宰割成不同的代码块,而后当路由被拜访的时候才加载对应的组件,这样就更加高效了。这样会大大提高首屏显示的速度,然而可能其余的页面的速度就会降下来

路由懒加载:

const Foo = () => import('./Foo.vue')
const router = new VueRouter({
  routes: [{ path: '/foo', component: Foo}
  ]
})

8. 第三方插件的按需引入

咱们在我的项目中常常会须要引入第三方插件,如果咱们间接引入整个插件,会导致我的项目的体积太大,咱们能够借助 babel-plugin-component,而后能够只引入须要的组件,以达到减小我的项目体积的目标。以下为我的项目中引入 element-ui 组件库为例

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)

9. 优化有限列表性能

如果你的利用存在十分长或者有限滚动的列表,那么须要采纳 虚构列表 的技术来优化性能,只须要渲染少部分区域的内容,缩小从新渲染组件和创立 dom 节点的工夫。你能够参考以下开源我的项目 vue-virtual-scroll-list (opens new window) 和 vue-virtual-scroller (opens new window)来优化这种有限列表的场景的

10. 服务端渲染 SSR or 预渲染

服务端渲染是指 Vue 在客户端将标签渲染成的整个 html 片段的工作在服务端实现,服务端造成的 html 片段间接返回给客户端这个过程就叫做服务端渲染。

  • 如果你的我的项目的 SEO首屏渲染 是评估我的项目的要害指标,那么你的我的项目就须要服务端渲染来帮忙你实现最佳的初始加载性能和 SEO
  • 如果你的 Vue 我的项目只需改善多数营销页面(例如 //about/contact 等)的 SEO,那么你可能须要预渲染,在构建时简略地生成针对特定路由的动态 HTML 文件。长处是设置预渲染更简略,并能够将你的前端作为一个齐全动态的站点,具体你能够应用 prerender-spa-plugin (opens new window) 就能够轻松地增加预渲染

Webpack 层面的优化

1. Webpack 对图片进行压缩

对小于 limit 的图片转化为 base64 格局,其余的不做操作。所以对有些较大的图片资源,在申请资源的时候,加载会很慢,咱们能够用 image-webpack-loader来压缩图片

npm install image-webpack-loader --save-dev
{test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
  use:[
    {
    loader: 'url-loader',
    options: {
      limit: 10000,
      name: utils.assetsPath('img/[name].[hash:7].[ext]')
      }
    },
    {
      loader: 'image-webpack-loader',
      options: {bypassOnDebug: true,}
    }
  ]
}

2. 缩小 ES6 转为 ES5 的冗余代码

Babel 插件会在将 ES6 代码转换成 ES5 代码时会注入一些辅助函数,例如上面的 ES6 代码

class HelloWebpack extends Component{...}

这段代码再被转换成能失常运行的 ES5 代码时须要以下两个辅助函数:

babel-runtime/helpers/createClass  // 用于实现 class 语法
babel-runtime/helpers/inherits  // 用于实现 extends 语法    

在默认状况下,Babel 会在每个输入文件中内嵌这些依赖的辅助函数代码,如果多个源代码文件都依赖这些辅助函数,那么这些辅助函数的代码将会呈现很屡次,造成代码冗余。为了不让这些辅助函数的代码反复呈现,能够在依赖它们时通过 require('babel-runtime/helpers/createClass') 的形式导入,这样就能做到只让它们呈现一次。babel-plugin-transform-runtime 插件就是用来实现这个作用的,将相干辅助函数进行替换成导入语句,从而减小 babel 编译进去的代码的文件大小

npm install babel-plugin-transform-runtime --save-dev

批改 .babelrc 配置文件为:

"plugins": ["transform-runtime"]

3. 提取公共代码

如果我的项目中没有去将每个页面的第三方库和公共模块提取进去,则我的项目会存在以下问题:

  • 雷同的资源被反复加载,节约用户的流量和服务器的老本。
  • 每个页面须要加载的资源太大,导致网页首屏加载迟缓,影响用户体验。

所以咱们须要将多个页面的公共代码抽离成独自的文件,来优化以上问题。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']
})

4. 模板预编译

  • 当应用 DOM 内模板或 JavaScript 内的字符串模板时,模板会在运行时被编译为渲染函数。通常状况下这个过程曾经足够快了,但对性能敏感的利用还是最好防止这种用法。
  • 预编译模板最简略的形式就是应用单文件组件——相干的构建设置会主动把预编译解决好,所以构建好的代码曾经蕴含了编译进去的渲染函数而不是原始的模板字符串。
  • 如果你应用 webpack,并且喜爱拆散 JavaScript 和模板文件,你能够应用 vue-template-loader (opens new window),它也能够在构建过程中把模板文件转换成为 JavaScript 渲染函数

5. 提取组件的 CSS

当应用单文件组件时,组件内的 CSS 会以 style 标签的形式通过 JavaScript 动静注入。这有一些小小的运行时开销,如果你应用服务端渲染,这会导致一段“无款式内容闪动 (fouc)”。将所有组件的 CSS 提取到同一个文件能够防止这个问题,也会让 CSS 更好地进行压缩和缓存

6. 优化 SourceMap

咱们在我的项目进行打包后,会将开发中的多个文件代码打包到一个文件中,并且通过压缩、去掉多余的空格、babel 编译化后,最终将编译失去的代码会用于线上环境,那么这样解决后的代码和源代码会有很大的差异,当有 bug 的时候,咱们只能定位到压缩解决后的代码地位,无奈定位到开发环境中的代码,对于开发来说不好调式定位问题,因而 sourceMap 呈现了,它就是为了解决不好调式代码问题的

SourceMap 的可选值如下(+ 号越多,代表速度越快,- 号越多,代表速度越慢, o 代表中等速度)

  • 开发环境举荐:cheap-module-eval-source-map
  • 生产环境举荐:cheap-module-source-map

起因如下:

  • cheap:源代码中的列信息是没有任何作用,因而咱们打包后的文件不心愿蕴含列相干信息,只有行信息能建设打包前后的依赖关系。因而不论是开发环境或生产环境,咱们都心愿增加 cheap 的根本类型来疏忽打包前后的列信息;
  • module:不论是开发环境还是正式环境,咱们都心愿能定位到 bug 的源代码具体的地位,比如说某个 Vue 文件报错了,咱们心愿能定位到具体的 Vue 文件,因而咱们也须要 module配置;
  • soure-mapsource-map 会为每一个打包后的模块生成独立的 soucemap 文件,因而咱们须要减少source-map 属性;
  • eval-source-mapeval 打包代码的速度十分快,因为它不生成 map 文件,然而能够对 eval 组合应用 eval-source-map 应用会将 map 文件以 DataURL 的模式存在打包后的 js 文件中。在正式环境中不要应用 eval-source-map, 因为它会减少文件的大小,然而在开发环境中,能够试用下,因为他们打包的速度很快。

7. 构建后果输入剖析

Webpack 输入的代码可读性十分差而且文件十分大,让咱们十分头疼。为了更简略、直观地剖析输入后果,社区中呈现了许多可视化剖析工具。这些工具以图形的形式将后果更直观地展现进去,让咱们疾速理解问题所在。接下来解说咱们在 Vue 我的项目中用到的剖析工具:webpack-bundle-analyzer

if (config.build.bundleAnalyzerReport) {var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
  webpackConfig.plugins.push(new BundleAnalyzerPlugin());
}

执行 $ npm run build --report 后生成剖析报告如下

根底的 Web 技术优化

1. 开启 gzip 压缩

gzipGNUzip 的缩写,最早用于 UNIX 零碎的文件压缩。HTTP 协定上的 gzip 编码是一种用来改良 web 应用程序性能的技术,web 服务器和客户端(浏览器)必须独特反对 gzip。目前支流的浏览器,Chrome,firefox,IE 等都反对该协定。常见的服务器如 Apache,Nginx,IIS 同样反对,zip 压缩效率十分高,通常能够达到 70% 的压缩率,也就是说,如果你的网页有 30K,压缩之后就变成了 9K 左右

以下咱们以服务端应用咱们相熟的 express 为例,开启 gzip 非常简单,相干步骤如下:

npm install compression --save
var compression = require('compression');
var app = express();
app.use(compression())

重启服务,察看网络面板外面的 response header,如果看到如下红圈里的字段则表明 gzip 开启胜利

Nginx 开启 gzip 压缩

# 是否启动 gzip 压缩,on 代表启动,off 代表开启
gzip  on;

#须要压缩的常见动态资源
gzip_types text/plain application/javascript   application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;

#因为 nginx 的压缩产生在浏览器端而微软的 ie6 很坑爹, 会导致压缩后图片看不见所以该选
项是禁止 ie6 产生压缩
gzip_disable "MSIE [1-6]\.";

#如果文件大于 1k 就启动压缩
gzip_min_length 1k;

#以 16k 为单位, 依照原始数据的大小以 4 倍的形式申请内存空间, 个别此项不要批改
gzip_buffers 4 16k;

#压缩的等级, 数字抉择范畴是 1 -9, 数字越小压缩的速度越快, 耗费 cpu 就越大
gzip_comp_level 2;

要想配置失效,记得重启 nginx 服务

nginx -t
nginx -s reload

2. 浏览器缓存

为了进步用户加载页面的速度,对动态资源进行缓存是十分必要的,依据是否须要从新向服务器发动申请来分类,将 HTTP 缓存规定分为两大类(强制缓存,比照缓存)

3. CDN 的应用

浏览器从服务器上下载 CSS、js 和图片等文件时都要和服务器连贯,而大部分服务器的带宽无限,如果超过限度,网页就半天反馈不过去。而 CDN 能够通过不同的域名来加载文件,从而使下载文件的并发连接数大大增加,且 CDN 具备更好的可用性,更低的网络提早和丢包率

4. 应用 Chrome Performance 查找性能瓶颈

ChromePerformance 面板能够录制一段时间内的 js 执行细节及工夫。应用 Chrome 开发者工具剖析页面性能的步骤如下。

  • 关上 Chrome 开发者工具,切换到 Performance 面板
  • 点击 Record 开始录制
  • 刷新页面或开展某个节点
  • 点击 Stop 进行录制

构建的 vue-cli 工程都到了哪些技术,它们的作用别离是什么

  • vue.jsvue-cli工程的外围,次要特点是 双向数据绑定 和 组件零碎。
  • vue-routervue官网举荐应用的路由框架。
  • vuex:专为 Vue.js 利用我的项目开发的状态管理器,次要用于保护 vue 组件间共用的一些 变量 和 办法。
  • axios(或者 fetchajax):用于发动 GET、或 POSThttp申请,基于 Promise 设计。
  • vuex等:一个专为 vue 设计的挪动端 UI 组件库。
  • 创立一个 emit.js 文件,用于 vue 事件机制的治理。
  • webpack:模块加载和 vue-cli 工程打包器。

既然 Vue 通过数据劫持能够精准探测数据变动,为什么还须要虚构 DOM 进行 diff 检测差别

  • 响应式数据变动,Vue的确能够在数据变动时,响应式零碎能够立即得悉。然而如果给每个属性都增加 watcher 用于更新的话,会产生大量的 watcher 从而升高性能
  • 而且粒度过细也得导致更新不精确的问题,所以 vue 采纳了组件级的 watcher 配合 diff 来检测差别

Vuex 有哪几种属性?

有五种,别离是 State、Getter、Mutation、Action、Module

  • state => 根本数据(数据源寄存地)
  • getters => 从根本数据派生进去的数据
  • mutations => 提交更改数据的办法,同步
  • actions => 像一个装璜器,包裹 mutations,使之能够异步。
  • modules => 模块化 Vuex

为什么 Vuex 的 mutation 中不能做异步操作?

  • Vuex 中所有的状态更新的惟一路径都是 mutation,异步操作通过 Action 来提交 mutation 实现,这样能够不便地跟踪每一个状态的变动,从而可能实现一些工具帮忙更好地理解咱们的利用。
  • 每个 mutation 执行实现后都会对应到一个新的状态变更,这样 devtools 就能够打个快照存下来,而后就能够实现 time-travel 了。如果 mutation 反对异步操作,就没有方法晓得状态是何时更新的,无奈很好的进行状态的追踪,给调试带来艰难。

vue-router 中如何爱护路由

剖析

路由爱护在利用开发过程中十分重要,简直每个利用都要做各种路由权限治理,因而相当考查使用者基本功。

体验

全局守卫:

const router = createRouter({...})
​
router.beforeEach((to, from) => {
  // ...
  // 返回 false 以勾销导航
  return false
})

路由独享守卫:

const routes = [
  {
    path: '/users/:id',
    component: UserDetails,
    beforeEnter: (to, from) => {
      // reject the navigation
      return false
    },
  },
]

组件内的守卫:

const UserDetails = {
  template: `...`,
  beforeRouteEnter(to, from) {// 在渲染该组件的对应路由被验证前调用},
  beforeRouteUpdate(to, from) {// 在以后路由扭转,然而该组件被复用时调用},
  beforeRouteLeave(to, from) {// 在导航来到渲染该组件的对应路由时调用},
}

答复

  • vue-router中爱护路由的办法叫做路由守卫,次要用来通过跳转或勾销的形式守卫导航。
  • 路由守卫有三个级别:全局 路由独享 组件级。影响范畴由大到小,例如全局的router.beforeEach(),能够注册一个全局前置守卫,每次路由导航都会通过这个守卫,因而在其外部能够退出管制逻辑决定用户是否能够导航到指标路由;在路由注册的时候能够退出单路由独享的守卫,例如beforeEnter,守卫只在进入路由时触发,因而只会影响这个路由,管制更准确;咱们还能够为路由组件增加守卫配置,例如beforeRouteEnter,会在渲染该组件的对应路由被验证前调用,管制的范畴更准确了。
  • 用户的任何导航行为都会走 navigate 办法,外部有个 guards 队列按程序执行用户注册的守卫钩子函数,如果没有通过验证逻辑则会勾销原有的导航。

原理

runGuardQueue(guards)链式的执行用户在各级别注册的守卫钩子函数,通过则持续下一个级别的守卫,不通过进入 catch 流程勾销本来导航

// 源码
runGuardQueue(guards)
  .then(() => {
    // check global guards beforeEach
    guards = []
    for (const guard of beforeGuards.list()) {guards.push(guardToPromiseFn(guard, to, from))
    }
    guards.push(canceledNavigationCheck)

    return runGuardQueue(guards)
  })
  .then(() => {
    // check in components beforeRouteUpdate
    guards = extractComponentsGuards(
      updatingRecords,
      'beforeRouteUpdate',
      to,
      from
    )

    for (const record of updatingRecords) {
      record.updateGuards.forEach(guard => {guards.push(guardToPromiseFn(guard, to, from))
      })
    }
    guards.push(canceledNavigationCheck)

    // run the queue of per route beforeEnter guards
    return runGuardQueue(guards)
  })
  .then(() => {
    // check the route beforeEnter
    guards = []
    for (const record of to.matched) {
      // do not trigger beforeEnter on reused views
      if (record.beforeEnter && !from.matched.includes(record)) {if (isArray(record.beforeEnter)) {for (const beforeEnter of record.beforeEnter)
            guards.push(guardToPromiseFn(beforeEnter, to, from))
        } else {guards.push(guardToPromiseFn(record.beforeEnter, to, from))
        }
      }
    }
    guards.push(canceledNavigationCheck)

    // run the queue of per route beforeEnter guards
    return runGuardQueue(guards)
  })
  .then(() => {// NOTE: at this point to.matched is normalized and does not contain any () => Promise<Component>

    // clear existing enterCallbacks, these are added by extractComponentsGuards
    to.matched.forEach(record => (record.enterCallbacks = {}))

    // check in-component beforeRouteEnter
    guards = extractComponentsGuards(
      enteringRecords,
      'beforeRouteEnter',
      to,
      from
    )
    guards.push(canceledNavigationCheck)

    // run the queue of per route beforeEnter guards
    return runGuardQueue(guards)
  })
  .then(() => {
    // check global guards beforeResolve
    guards = []
    for (const guard of beforeResolveGuards.list()) {guards.push(guardToPromiseFn(guard, to, from))
    }
    guards.push(canceledNavigationCheck)

    return runGuardQueue(guards)
  })
  // catch any navigation canceled
  .catch(err =>
    isNavigationFailure(err, ErrorTypes.NAVIGATION_CANCELLED)
      ? err
      : Promise.reject(err)
  )

源码地位(opens new window)

Vue3.0 有什么更新

(1)监测机制的扭转

  • 3.0 将带来基于代理 Proxy 的 observer 实现,提供全语言笼罩的反馈性跟踪。
  • 打消了 Vue 2 当中基于 Object.defineProperty 的实现所存在的很多限度:

(2)只能监测属性,不能监测对象

  • 检测属性的增加和删除;
  • 检测数组索引和长度的变更;
  • 反对 Map、Set、WeakMap 和 WeakSet。

(3)模板

  • 作用域插槽,2.x 的机制导致作用域插槽变了,父组件会从新渲染,而 3.0 把作用域插槽改成了函数的形式,这样只会影响子组件的从新渲染,晋升了渲染的性能。
  • 同时,对于 render 函数的方面,vue3.0 也会进行一系列更改来不便习惯间接应用 api 来生成 vdom。

(4)对象式的组件申明形式

  • vue2.x 中的组件是通过申明的形式传入一系列 option,和 TypeScript 的联合须要通过一些装璜器的形式来做,尽管能实现性能,然而比拟麻烦。
  • 3.0 批改了组件的申明形式,改成了类式的写法,这样使得和 TypeScript 的联合变得很容易

(5)其它方面的更改

  • 反对自定义渲染器,从而使得 weex 能够通过自定义渲染器的形式来扩大,而不是间接 fork 源码来改的形式。
  • 反对 Fragment(多个根节点)和 Protal(在 dom 其余局部渲染组建内容)组件,针对一些非凡的场景做了解决。
  • 基于 tree shaking 优化,提供了更多的内置性能。

params 和 query 的区别

用法:query 要用 path 来引入,params 要用 name 来引入,接管参数都是相似的,别离是 this.$route.query.namethis.$route.params.name

url 地址显示:query 更加相似于 ajax 中 get 传参,params 则相似于 post,说的再简略一点,前者在浏览器地址栏中显示参数,后者则不显示

留神:query 刷新不会失落 query 外面的数据 params 刷新会失落 params 外面的数据。

函数式组件劣势和原理

函数组件的特点

  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. 函数式组件构造比较简单,代码构造更清晰

应用场景:

  • 一个简略的展现组件,作为容器组件应用 比方 router-view 就是一个函数式组件
  • “高阶组件”——用于接管一个组件作为参数,返回一个被包装过的组件

例子

Vue.component('functional',{ // 构造函数产生虚构节点的
    functional:true, // 函数式组件 // data={attrs:{}}
    render(h){return h('div','test')
    }
})
const vm = new Vue({el: '#app'})

源码相干

// functional component
if (isTrue(Ctor.options.functional)) { // 带有 functional 的属性的就是函数式组件
  return createFunctionalComponent(Ctor, propsData, data, context, children)
}

// extract listeners, since these needs to be treated as
// child component listeners instead of DOM listeners
const listeners = data.on // 处理事件
// replace with listeners with .native modifier
// so it gets processed during parent component patch.
data.on = data.nativeOn // 解决原生事件

// install component management hooks onto the placeholder node
installComponentHooks(data) // 装置组件相干钩子(函数式组件没有调用此办法,从而性能高于一般组件)

如何在组件中批量应用 Vuex 的 getter 属性

应用 mapGetters 辅助函数, 利用对象开展运算符将 getter 混入 computed 对象中

import {mapGetters} from 'vuex'
export default{
    computed:{...mapGetters(['total','discountTotal'])
    }
}

实现双向绑定

咱们还是以 Vue 为例,先来看看 Vue 中的双向绑定流程是什么的

  1. new Vue()首先执行初始化,对 data 执行响应化解决,这个过程产生 Observe
  2. 同时对模板执行编译,找到其中动静绑定的数据,从 data 中获取并初始化视图,这个过程产生在 Compile
  3. 同时定义⼀个更新函数和 Watcher,未来对应数据变动时Watcher 会调用更新函数
  4. 因为 data 的某个 key 在⼀个视图中可能呈现屡次,所以每个 key 都须要⼀个管家 Dep 来治理多个Watcher
  5. 未来 data 中数据⼀旦发生变化,会首先找到对应的 Dep,告诉所有Watcher 执行更新函数

流程图如下:

先来一个构造函数:执行初始化,对 data 执行响应化解决

class Vue {constructor(options) {  
    this.$options = options;  
    this.$data = options.data;  

    // 对 data 选项做响应式解决  
    observe(this.$data);  

    // 代理 data 到 vm 上  
    proxy(this);  

    // 执行编译  
    new Compile(options.el, this);  
  }  
}  

data 选项执行响应化具体操作

function observe(obj) {if (typeof obj !== "object" || obj == null) {return;}  
  new Observer(obj);  
}  

class Observer {constructor(value) {  
    this.value = value;  
    this.walk(value);  
  }  
  walk(obj) {Object.keys(obj).forEach((key) => {defineReactive(obj, key, obj[key]);  
    });  
  }  
}  

编译Compile

对每个元素节点的指令进行扫描跟解析, 依据指令模板替换数据, 以及绑定相应的更新函数

class Compile {constructor(el, vm) {  
    this.$vm = vm;  
    this.$el = document.querySelector(el);  // 获取 dom  
    if (this.$el) {this.compile(this.$el);  
    }  
  }  
  compile(el) {  
    const childNodes = el.childNodes;   
    Array.from(childNodes).forEach((node) => { // 遍历子元素  
      if (this.isElement(node)) {   // 判断是否为节点  
        console.log("编译元素" + node.nodeName);  
      } else if (this.isInterpolation(node)) {console.log("编译插值⽂本" + node.textContent);  // 判断是否为插值文本 {{}}  
      }  
      if (node.childNodes && node.childNodes.length > 0) {  // 判断是否有子元素  
        this.compile(node);  // 对子元素进行递归遍历  
      }  
    });  
  }  
  isElement(node) {return node.nodeType == 1;}  
  isInterpolation(node) {return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent);  
  }  
}  

依赖收集

视图中会用到 data 中某 key,这称为依赖。同⼀个key 可能呈现屡次,每次都须要收集进去用⼀个 Watcher 来保护它们,此过程称为依赖收集多个 Watcher 须要⼀个 Dep 来治理,须要更新时由 Dep 统⼀告诉

实现思路

  1. defineReactive时为每⼀个 key 创立⼀个 Dep 实例
  2. 初始化视图时读取某个key,例如name1,创立⼀个watcher1
  3. 因为触发 name1getter办法,便将 watcher1 增加到 name1 对应的 Dep
  4. name1 更新,setter触发时,便可通过对应 Dep 告诉其治理所有 Watcher 更新
// 负责更新视图  
class Watcher {constructor(vm, key, updater) {  
    this.vm = vm  
    this.key = key  
    this.updaterFn = updater  

    // 创立实例时,把以后实例指定到 Dep.target 动态属性上  
    Dep.target = this  
    // 读一下 key,触发 get  
    vm[key]  
    // 置空  
    Dep.target = null  
  }  

  // 将来执行 dom 更新函数,由 dep 调用的  
  update() {this.updaterFn.call(this.vm, this.vm[this.key])  
  }  
}  

申明Dep

class Dep {constructor() {this.deps = [];  // 依赖治理  
  }  
  addDep(dep) {this.deps.push(dep);  
  }  
  notify() {this.deps.forEach((dep) => dep.update());  
  }  
} 

创立 watcher 时触发getter

class Watcher {constructor(vm, key, updateFn) {  
    Dep.target = this;  
    this.vm[this.key];  
    Dep.target = null;  
  }  
}  

依赖收集,创立 Dep 实例

function defineReactive(obj, key, val) {this.observe(val);  
  const dep = new Dep();  
  Object.defineProperty(obj, key, {get() {Dep.target && dep.addDep(Dep.target);// Dep.target 也就是 Watcher 实例  
      return val;  
    },  
    set(newVal) {if (newVal === val) return;  
      dep.notify(); // 告诉 dep 执行更新办法},  
  });  
}  

用过 pinia 吗?有什么长处?

1. pinia 是什么?

  • Vue3 中,能够应用传统的 Vuex 来实现状态治理,也能够应用最新的 pinia 来实现状态治理,咱们来看看官网如何解释 pinia 的:PiniaVue 的存储库,它容许您跨组件 / 页面共享状态。
  • 实际上,pinia就是 Vuex 的升级版,官网也说过,为了尊重原作者,所以取名 pinia,而没有取名Vuex,所以大家能够间接将pinia 比作为 Vue3Vuex

2. 为什么要应用 pinia?

  • Vue2Vue3 都反对,这让咱们同时应用 Vue2Vue3的小伙伴都能很快上手。
  • pinia中只有 stategetteraction,摈弃了Vuex 中的 MutationVuexmutation始终都不太受小伙伴们的待见,pinia间接摈弃它了,这无疑缩小了咱们工作量。
  • piniaaction 反对同步和异步,Vuex不反对
  • 良好的 Typescript 反对,毕竟咱们 Vue3 都举荐应用 TS 来编写,这个时候应用 pinia 就十分适合了
  • 无需再创立各个模块嵌套了,Vuex中如果数据过多,咱们通常分模块来进行治理,稍显麻烦,而 pinia 中每个 store 都是独立的,相互不影响。
  • 体积十分小,只有 1KB 左右。
  • pinia反对插件来扩大本身性能。
  • 反对服务端渲染

3. pinna 应用

pinna 文档(opens new window)

  1. 筹备工作

咱们这里搭建一个最新的 Vue3 + TS + Vite 我的项目

npm create [email protected] my-vite-app --template vue-ts
  1. pinia根底应用
yarn add pinia
// main.ts
import {createApp} from "vue";
import App from "./App.vue";
import {createPinia} from "pinia";
const pinia = createPinia();

const app = createApp(App);
app.use(pinia);
app.mount("#app");

2.1 创立store

//sbinsrc/store/user.ts
import {defineStore} from 'pinia'

// 第一个参数是应用程序中 store 的惟一 id
export const useUsersStore = defineStore('users', {// 其它配置项})

创立 store 很简略,调用 p inia中的 defineStore 函数即可,该函数接管两个参数:

  • name:一个字符串,必传项,该 store 的惟一id
  • options:一个对象,store的配置项,比方配置 store 内的数据,批改数据的办法等等。

咱们能够定义任意数量的 store,因为咱们其实一个store 就是一个函数,这也是 pinia 的益处之一,让咱们的代码扁平化了,这和 Vue3 的实现思维是一样的

2.2 应用store

<!-- src/App.vue -->
<script setup lang="ts">
import {useUsersStore} from "../src/store/user";
const store = useUsersStore();
console.log(store);
</script>

2.3 增加state

export const useUsersStore = defineStore("users", {state: () => {
    return {
      name: "test",
      age: 20,
      sex: "男",
    };
  },
});

2.4 读取 state 数据

<template>
  <img alt="Vue logo" src="./assets/logo.png" />
  <p> 姓名:{{name}}</p>
  <p> 年龄:{{age}}</p>
  <p> 性别:{{sex}}</p>
</template>
<script setup lang="ts">
import {ref} from "vue";
import {useUsersStore} from "../src/store/user";
const store = useUsersStore();
const name = ref<string>(store.name);
const age = ref<number>(store.age);
const sex = ref<string>(store.sex);
</script>

上段代码中咱们间接通过 store.age 等形式获取到了 store 存储的值,然而大家有没有发现,这样比拟繁琐,咱们其实能够用解构的形式来获取值,使得代码更简洁一点

import {useUsersStore, storeToRefs} from "../src/store/user";
const store = useUsersStore();
const {name, age, sex} = storeToRefs(store); // storeToRefs 获取的值是响应式的

2.5 批改 state 数据

<template>
  <img alt="Vue logo" src="./assets/logo.png" />
  <p> 姓名:{{name}}</p>
  <p> 年龄:{{age}}</p>
  <p> 性别:{{sex}}</p>
  <button @click="changeName"> 更改姓名 </button>
</template>
<script setup lang="ts">
import child from './child.vue';
import {useUsersStore, storeToRefs} from "../src/store/user";
const store = useUsersStore();
const {name, age, sex} = storeToRefs(store);
const changeName = () => {
  store.name = "张三";
  console.log(store);
};
</script>

2.6 重置state

  • 有时候咱们批改了 state 数据,想要将它还原,这个时候该怎么做呢?就比方用户填写了一部分表单,忽然想重置为最初始的状态。
  • 此时,咱们间接调用 store$reset()办法即可,持续应用咱们的例子,增加一个重置按钮
<button @click="reset"> 重置 store</button>
// 重置 store
const reset = () => {store.$reset();
};

当咱们点击重置按钮时,store中的数据会变为初始状态,页面也会更新

2.7 批量更改 state 数据

如果咱们一次性须要批改很多条数据的话,有更加简便的办法,应用 store$patch办法,批改 app.vue 代码,增加一个批量更改数据的办法

<button @click="patchStore"> 批量批改数据 </button>
// 批量批改数据
const patchStore = () => {
  store.$patch({
    name: "张三",
    age: 100,
    sex: "女",
  });
};
  • 有教训的小伙伴可能发现了,咱们采纳这种批量更改的形式仿佛代价有一点大,如果咱们 state 中有些字段无需更改,然而依照上段代码的写法,咱们必须要将 state 中的所有字段例举出了。
  • 为了解决该问题,pinia提供的 $patch 办法还能够接管一个回调函数,它的用法有点像咱们的数组循环回调函数了。
store.$patch((state) => {state.items.push({ name: 'shoes', quantity: 1})
  state.hasChanged = true
})

2.8 间接替换整个state

pinia提供了办法让咱们间接替换整个 state 对象,应用 store$state办法

store.$state = {counter: 666, name: '张三'}

上段代码会将咱们提前申明的 state 替换为新的对象,可能这种场景用得比拟少

  1. getters属性
  2. gettersdefineStore 参数配置项外面的另一个属性
  3. 能够把 getter 设想成 Vue 中的计算属性,它的作用就是返回一个新的后果,既然它和 Vue 中的计算属性相似,那么它必定也是会被缓存的,就和 computed 一样

3.1 增加getter

export const useUsersStore = defineStore("users", {state: () => {
    return {
      name: "test",
      age: 10,
      sex: "男",
    };
  },
  getters: {getAddAge: (state) => {return state.age + 100;},
  },
})

上段代码中咱们在配置项参数中增加了 getter 属性,该属性对象中定义了一个 getAddAge 办法,该办法会默认接管一个 state 参数,也就是 state 对象,而后该办法返回的是一个新的数据

3.2 应用getter

<template>
  <p> 新年龄:{{store.getAddAge}}</p>
  <button @click="patchStore"> 批量批改数据 </button>
</template>
<script setup lang="ts">
import {useUsersStore} from "../src/store/user";
const store = useUsersStore();
// 批量批改数据
const patchStore = () => {
  store.$patch({
    name: "张三",
    age: 100,
    sex: "女",
  });
};
</script>

上段代码中咱们间接在标签上应用了 store.gettAddAge 办法,这样能够保障响应式,其实咱们 state 中的 name 等属性也能够以此种形式间接在标签上应用,也能够放弃响应式

3.3 getter中调用其它getter

export const useUsersStore = defineStore("users", {state: () => {
    return {
      name: "test",
      age: 20,
      sex: "男",
    };
  },
  getters: {getAddAge: (state) => {return state.age + 100;},
    getNameAndAge(): string {return this.name + this.getAddAge; // 调用其它 getter},
  },
});

3.3 getter传参

export const useUsersStore = defineStore("users", {state: () => {
    return {
      name: "test",
      age: 20,
      sex: "男",
    };
  },
  getters: {getAddAge: (state) => {return (num: number) => state.age + num;
    },
    getNameAndAge(): string {return this.name + this.getAddAge; // 调用其它 getter},
  },
});
<p> 新年龄:{{store.getAddAge(1100) }}</p>
  1. actions属性
  2. 后面咱们提到的 stategetter s 属性都次要是数据层面的,并没有具体的业务逻辑代码,它们两个就和咱们组件代码中的 data 数据和 computed 计算属性一样。
  3. 那么,如果咱们有业务代码的话,最好就是卸载 actions 属性外面,该属性就和咱们组件代码中的 methods 类似,用来搁置一些解决业务逻辑的办法。
  4. actions属性值同样是一个对象,该对象外面也是存储的各种各样的办法,包含同步办法和异步办法

4.1 增加actions

export const useUsersStore = defineStore("users", {state: () => {
    return {
      name: "test",
      age: 20,
      sex: "男",
    };
  },
  getters: {getAddAge: (state) => {return (num: number) => state.age + num;
    },
    getNameAndAge(): string {return this.name + this.getAddAge; // 调用其它 getter},
  },
  actions: {
    // 在理论场景中,该办法能够是任何逻辑,比方发送申请、存储 token 等等。大家把 actions 办法当作一个一般的办法即可,非凡之处在于该办法外部的 this 指向的是以后 store
    saveName(name: string) {this.name = name;},
  },
});

4.2 应用actions

应用 actions 中的办法也非常简单,比方咱们在 App.vue 中想要调用该办法

const saveName = () => {store.saveName("poetries");
};

总结

pinia的知识点很少,如果你有 Vuex 根底,那么学起来更是大海捞针

pinia 无非就是以下 3 个大点:

  • state
  • getters
  • actions
正文完
 0