10-讲讲vue-element-admin开源项目

6,160 阅读14分钟

项目配置

vue.config.js

在根目录下创建vue.config.js,做一些相应的配置

//为什么是module.exports,因为此文件的执行环境是node环境
module.exports={
   publicPath:'/best-practice',//部署应用包时的基本URL
   devServer:{
       port:port
   },
   configureWebpack:{
       // 想index.html注入标题
       name:'vue最佳实践'
   }
}
// index.html——使用webpack中配置的name
<title><%= webpackConfig.name %></title>

优雅使用icon——svg

icon发展史

  1. 最初的时候,大部分图标都是用img来实现的,如果一个项目中涉及到很多图标的时候,那一个页面中的请求资源中img占了大部分;
  2. 后面慢慢出现了雪碧图,将很多图标整合到一个图片上,css利用background-position 定位显示不同的 icon 图标,比起最开始img的方式,雪碧图已经有了很大的优化,但是依然有一个非常大的痛点,那就是每新增一个图标,就得去更新原先的雪碧图,这样做风险是很大的,不小心影响了原先图标的位置时,那页面中某些图标就加载不出来了,这种方式是极难维护的。
  3. Font Awesome 出现,Font Awesome官网的体验不是太好,图标很小,找起来相当费力。
  4. iconfont出现,阿里的开源库,找起来相当轻松,而且图标数量相当可人。

iconfont使用方式

先总体讲一下,在iconfont官网上将需要的图标打包下载下来后,如下:

将上面的这些包放到项目中,如果仅仅用unicode和font-class的方式来展示图标的话,那么仅仅需要引入iconfont.css即可,如果需要增加图标,新下载了图标包下来后,只需要将iconfont.css文件更新一下即可。

如果使用svg的方式展示图标的话,这时候起作用的是iconfont.js文件,如果图标有更新的话,将iconfont.js文件替换掉就行。

  • unicode

    Unicode 是字体在网页端最原始的应用方式,特点是:

    • 兼容性最好,支持 IE6+,及所有现代浏览器。
    • 支持按字体的方式去动态调整图标大小,颜色等等。
    • 但是因为是字体,所以不支持多色。只能使用平台里单色的图标,就算项目里有多色图标也会自动去色。

    注意:新版 iconfont 支持多色图标,这些多色图标在 Unicode 模式下将不能使用,如果有需求建议使用symbol 的引用方式

    Unicode 使用步骤如下:

    第一步:引入iconfont.css文件

    // iconfont.css
    @font-face {font-family: "iconfont";
      src: url('iconfont.eot?t=1593657248234'); /* IE9 */
      src: url('iconfont.eot?t=1593657248234#iefix') format('embedded-opentype'), /* IE6-IE8 */
      //更新的时候最重要的是更新这一部分
      url('data:application/x-font-woff2;charset=utf-8;base64,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') format('woff2'),
      url('iconfont.woff?t=1593657248234') format('woff'),
      url('iconfont.ttf?t=1593657248234') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */
      url('iconfont.svg?t=1593657248234#iconfont') format('svg'); /* iOS 4.1- */
    }
    
    .iconfont {
      font-family: "iconfont" !important;
      font-size: 16px;
      font-style: normal;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
    }
    
    .icon-yundong-:before {
      content: "\e611";
    }
    
    .icon-yundong:before {
      content: "\e6ea";
    }
    
    .icon-zhoubao:before {
      content: "\e68c";
    }
    
    // 新增的图标后面直接加入即可
    .icon-meilishuo:before {
      content: "\ea15";
    }
    
    .icon-meilishuo1:before {
      content: "\e78e";
    }
    

    第二步:挑选相应图标并获取字体编码,应用于页面

    <span class="iconfont">&#x33;</span>
    
  • font-class

    font-class 是 Unicode 使用方式的一种变种,主要是解决 Unicode 书写不直观,语意不明确的问题。

    与 Unicode 使用方式相比,具有如下特点:

    • 兼容性良好,支持 IE8+,及所有现代浏览器。
    • 相比于 Unicode 语意明确,书写更直观。可以很容易分辨这个 icon 是什么。
    • 因为使用 class 来定义图标,所以当要替换图标时,只需要修改 class 里面的 Unicode 引用。
    • 不过因为本质上还是使用的字体,所以多色图标还是不支持的。

    font-class 使用步骤如下:

    第一步:引入iconfont.css文件

    <link rel="stylesheet" href="./iconfont.css">
    

    注意:修改iconfont.css文件的方式与unicode的方式是一样的

    第二步:挑选相应图标并获取类名,应用于页面:

    <span class="iconfont icon-xxx"></span>
    
  • symbol

    这是一种全新的使用方式,应该说这才是未来的主流,也是平台目前推荐的用法。相关介绍可以参考这篇文章 这种用法其实是做了一个 SVG 的集合,与另外两种相比具有如下特点:

    • 支持多色图标了,不再受单色限制。
    • 通过一些技巧,支持像字体那样,通过 font-size, color 来调整样式。
    • 兼容性较差,支持 IE9+,及现代浏览器。
    • 浏览器渲染 SVG 的性能一般,还不如 png。

    使用步骤如下:

    第一步:引入项目下面生成的 symbol 代码:

    <script src="./iconfont.js"></script>
    

    第二步:加入通用 CSS 代码(引入一次就行):

    .icon {
      width: 1em;
      height: 1em;
      vertical-align: -0.15em;
      fill: currentColor;
      overflow: hidden;
    }
    

    第三步:挑选相应图标并获取类名,应用于页面:

    <svg class="icon" aria-hidden="true">
      <use xlink:href="#icon-xxx"></use>
    </svg>
    

vue项目中使用svg

使用svg好处
  • 支持多色图标了,不再受单色限制。
  • 支持像字体那样通过font-size,color来调整样式。
  • 支持 ie9+
  • 可利用CSS实现动画。
  • 减少HTTP请求。
  • 矢量,缩放不失真
  • 可以很精细的控制SVG图标的每一部分

使用svg-icon的好处是我再也不用发送woff|eot|ttf| 这些很多个字体库请求了,所有的svg都可以内联在html内。
svg是真正的矢量图,放大缩小都不会失真。

安装loader
mpm i svg-sprite-loader -D

svg-sprite-loader 将加载的 svg 图片拼接成雪碧图,放到页面中,其它地方通过 复用。

本来只添加svg-sprite-loader就行了,但是svg也是图片的一种,所以file-loader也会对其进行处理,所以就会冲突,解决的办法就是,在项目中新建一个文件icons,使用file-loader编译svg的时候不编译icons里面的图标

添加配置——vue.config.js

vue inspect --rules:可查看webpack中有哪些相关的规则

[
  'vue',
  'images',
  'svg',
  'media',
  'fonts',
  'pug',
  'css',
  'postcss',
  'scss',
  'sass',
  'less',
  'stylus',
  'js',
  'eslint',
  'icons'
]

vue inspect --rule svg:单独查看某一项在webpack中的配置

// 对svg处理的默认配置:使用file-loader
{
  test: /\.(svg)(\?.*)?$/,
  use: [
    {
      loader: 'file-loader',
      options: {
        name: 'static/img/[name].[hash:8].[ext]'
      }
    }
  ]
}

添加链式操作

vue-cli默认情况下使用file-loader对svg进行处理,会将svg放到/img目录下,现在需要更改webpack配置,使用svg-sprite-loader处理svg`。

  1. 直接将,默认配置中的test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,中的svg删掉,不过这样处理是有风险的,因为项目中可能有的svg确实需要当图片资源使用,而且第三方库中也有可能使用到svg

  2. 在webpack中重新做配置,只处理需要处理的那一部分svg

module.exports={
   publicPath:'/best-practice',//部署应用包时的基本URL
   devServer:{
       port:port
   },
   configureWebpack:{
       // 想index.html注入标题
       name:'vue最佳实践'
   },
   chainWebpack(config){
     // set svg-sprite-loader
    // 1.svg rule中要排除icons目录
    config.module
      .rule('svg')
      .exclude.add(resolve('src/icons'))
      .end()
    // 2加一个规则icons
    config.module
      .rule('icons')
      .test(/\.svg$/)
      .include.add(resolve('src/icons'))
      .end()
      .use('svg-sprite-loader')
      .loader('svg-sprite-loader')
      .options({
        symbolId: 'icon-[name]'
      })
      .end()  
   }
}
自动导入svg图标

// icons/index.js
const req = require.context('./svg', false, /\.svg$/)
req.keys().map(req);

// main.js
import './icons'

使用:

<svg class="icon" aria-hidden="true">
  <use xlink:href="#icon-xxx"></use>
</svg>

其实到这一步,项目中已经可以随意使用svg图标了,为了能统一处理svg图标的样式以及代码的可阅读性,咱们一起去创建一个svg的公用组件去~

创建svg组件
  • svg组件
<template>
  <svg :class="svgClass" aria-hidden="true">
    <use :href="iconName" />
  </svg>
</template>

<script>
// doc: https://panjiachen.github.io/vue-element-admin-site/feature/component/svg-icon.html#usage
import { isExternal } from '@/utils/validate'

export default {
  name: 'SvgIcon',
  props: {
    iconClass: {
      type: String,
      required: true
    },
    className: {
      type: String,
      default: ''
    }
  },
  computed: {
    isExternal () {
      return isExternal(this.iconClass)
    },
    iconName () {
      return `#icon-${this.iconClass}`
    },
    svgClass () {
      if (this.className) {
        return 'svg-icon ' + this.className
      } else {
        return 'svg-icon'
      }
    },
    styleExternalIcon () {
      return {
        mask: `url(${this.iconClass}) no-repeat 50% 50%`,
        '-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`
      }
    }
  }
}
</script>

<style scoped>
.svg-icon {
  width: 1em;
  height: 1em;
  vertical-align: -0.15em;
  fill: currentColor;
  overflow: hidden;
}

.svg-external-icon {
  background-color: currentColor;
  mask-size: cover!important;
  display: inline-block;
}
</style>
  • 全局注册
import Vue from 'vue'
import SvgIcon from '@/components/SvgIcon'

// 全局注册svg组件
Vue.component('svg-icon', SvgIcon)
  • 使用
<svg-icon icon-class="keeping"></svg-icon>

权限控制及动态路由

路由定义

路由分为两种:constantRoutesasyncRoutes

constantRoutes是常规路由,不需要守卫,用户可直接访问

export const constantRoutes = [
  {
    path: '/login',
    component: () => import('@/views/login/index'),
    hidden: true
  },
  {
    path: '/404',
    component: () => import('@/views/error-page/404'),
    hidden: true
  },
  {
    path: '/401',
    component: () => import('@/views/error-page/401'),
    hidden: true
  }
]

asyncRoutes:权限页面,用户需要登录并且拥有访问的权限角色才能访问

export const asyncRoutes = [
  {
    path: "/about",
    component: Layout,
    redirect: "/about/index",
    children: [
      {
        path: "index",
        component: () =>
          import(/* webpackChunkName: "home" */ "@/views/About.vue"),
        name: "about",
        meta: {
            title: "About",
            icon: "qq",
            roles: ['admin', 'editor']
        },
      }
    ]
  }
]

export default new Router({
  mode: "history",
  base: process.env.BASE_URL,
  routes: constRoutes
})

路由守卫

// 白名单
const whiteList = ['/login', '/auth-redirect'] // no redirect whitelist

if (whiteList.indexOf(to.path) !== -1) {
  // 白名单, go directly
  next()
} else {
  api.shadow().then(async () => {
      const hasRoles = store.getters.roles && store.getters.roles.length > 0
      if (hasRoles) {
        next()
      } else {
        try {
          // 获取权限
          const roles = await store.dispatch('user/getInfo')
          resetRouter()
          // 挂载路由
          const accessRoutes = await store.dispatch('permission/generateRoutes', roles)

          router.addRoutes(accessRoutes)
          next({
            ...to,
            replace: true
          }) // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
        } catch (error) {
          next('login')
        }
      }
    }).catch(() => {
      next('login')
    })
}

添加动态路由

根据用户角色过滤出可访问的路由并动态添加到router。

创建permission模块,store/modules/permission.js

import { asyncRoutes, constantRoutes } from '@/router'

/**
 * Use meta.role to determine if the current user has permission
 * @param roles
 * @param route
 */
function hasPermission(roles, route) {
  if (route.meta && route.meta.roles) {
    return roles.some(role => route.meta.roles.includes(role))
  } else {
    return true
  }
}

/**
 * Filter asynchronous routing tables by recursion
 * @param routes asyncRoutes
 * @param roles
 */
export function filterAsyncRoutes(routes, roles) {
  const res = []

  routes.forEach(route => {
    const tmp = { ...route }
    if (hasPermission(roles, tmp)) {
      if (tmp.children) {
        tmp.children = filterAsyncRoutes(tmp.children, roles)
      }
      res.push(tmp)
    }
  })

  return res
}

const state = {
  routes: [],
  addRoutes: []
}

const mutations = {
  SET_ROUTES: (state, routes) => {
    state.addRoutes = routes
    state.routes = constantRoutes.concat(routes)
  }
}

const actions = {
  generateRoutes({ commit }, roles) {
    return new Promise(resolve => {
      let accessedRoutes
      if (roles.includes('admin')) {
        accessedRoutes = asyncRoutes || []
      } else {
        accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
      }
      commit('SET_ROUTES', accessedRoutes)
      resolve(accessedRoutes)
    })
  }
}

export default {
  namespaced: true,
  state,
  mutations,
  actions
}

注意:我在公司项目里面生成动态路由的时候就犯难了,思考了很多,把有权限的路由与菜单栏展示一起考虑了,用户既有权限访问这些有权限的路由,然而某些有权限的路由还不能在菜单栏上呈现出来,比如有一些二级的详情页,菜单栏上不需要呈现这一项菜单的,当时候思考了很多,思绪也比较混乱,这次重新整理这部分的时候,思路突然清晰了很多,动态路由就是将所有有权限的路由都挂载上,这里不要跟菜单栏混淆,菜单栏渲染的那里有自己的逻辑处理,这样就各自处理各自的逻辑就好了。

按钮权限

  • 封装按钮权限指令

封装一个指令v-permission,从而实现按钮级别的权限控制,在src下创建directive目录,创建permission.js

// src/directive/permission.js
import store from '@/store'

export default {
  inserted(el, binding, vnode) {
    const { value } = binding
    const roles = store.getters && store.getters.roles

    if (value && value instanceof Array && value.length > 0) {
      const permissionRoles = value

      const hasPermission = roles.some(role => {
        return permissionRoles.includes(role)
      })

      if (!hasPermission) {
        el.parentNode && el.parentNode.removeChild(el)
      }
    } else {
      throw new Error(`need roles! Like v-permission="['admin','editor']"`)
    }
  }
}
  • 全局注册指令

main.js中,将自定义的权限指令全局注册

import permission from "./directive/permission";
Vue.directive("permission", permission);
  • v-permission指令的使用
<button v-permission="['admin']">admin button</button>
  • element中el-tabs使用指令无效的问题

上面封装的v-permission指令,只能删除挂载指令的元素,对于额外生成的和指令无关的元素是不生效的

<el-tabs>
    <el-tab-pane label="用户管理" name="first" v-permission="['admin', 'editor']">用户管理</el-tab-pane>
    <el-tab-pane label="配置管理" name="second" v-permission="['admin', 'editor']">配置管理</el-tab-pane>
    <el-tab-pane label="角色管理" name="third" v-permission="['admin']">角色管理 </el-tab-pane>
    <el-tab-pane label="定时任务补偿" name="fourth" v-permission="['admin', 'editor']">定时任务补偿</el-tab-pane>
</el-tabs>

el-tab-pane标签上加上v-permission指令后,根据上图中生成的代码,可发现,也仅仅是将此tab下的内容设置了权限,而几个可切换的tab并没有受到权限的控制,你会发现页面展示的时候,3个tab都存在,也可正常切换,但是每个tab下的内容会根据权限来展示与否,虽然也算是曲线实现了权限控制这个功能,但是总是不太优雅,此时只能用v-if来实现这个功能了:

utils文件下创建一个checkPermission公用的方法:

import store from '@/store'

/**
 * @param {Array} value
 * @returns {Boolean}
 * @example see @/views/permission/directive.vue
 */
export default function checkPermission(value) {
  if (value && value instanceof Array && value.length > 0) {
    const roles = store.getters && store.getters.roles
    const permissionRoles = value

    const hasPermission = roles.some(role => {
      return permissionRoles.includes(role)
    })

    if (!hasPermission) {
      return false
    }
    return true
  } else {
    console.error(`need roles! Like v-permission="['admin','editor']"`)
    return false
  }
}
  • v-if实现权限校验的使用
<template>
    <el-tabs>
        <el-tab-pane v-if="checkPermission(['admin'])">
    </el-tabs>
</template>

导航菜单生成

导航菜单是根据路由信息并结合权限判断而动态生成的。它需要支持路由的多级嵌套,所以这里要用到递归组件。

创建:layout/Sidebar/index.vue

<template>
  <div :class="{'has-logo':showLogo}">
    <logo v-if="showLogo" :collapse="isCollapse" />
    <el-scrollbar wrap-class="scrollbar-wrapper">
      <el-menu
        :default-active="activeMenu"
        :collapse="isCollapse"
        :background-color="variables.menuBg"
        :text-color="variables.menuText"
        :unique-opened="false"
        :active-text-color="variables.menuActiveText"
        :collapse-transition="false"
        mode="vertical"
      >
        <sidebar-item v-for="route in permission_routes" :key="route.path" :item="route" :base-path="route.path" />
      </el-menu>
    </el-scrollbar>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'
import Logo from './Logo'
import SidebarItem from './SidebarItem'
import variables from '@/styles/variables.scss'

export default {
  components: { SidebarItem, Logo },
  computed: {
    ...mapGetters([
      'permission_routes',
      'sidebar'
    ]),
    activeMenu() {
      const route = this.$route
      const { meta, path } = route
      // if set path, the sidebar will highlight the path you set
      if (meta.activeMenu) {
        return meta.activeMenu
      }
      return path
    },
    showLogo() {
      return this.$store.state.settings.sidebarLogo
    },
    variables() {
      return variables
    },
    isCollapse() {
      return !this.sidebar.opened
    }
  }
}
</script>

创建:layout/Sidebar/SidebarItem.vue

<template>
  <div v-if="!item.hidden">
    <template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
      <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
        <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">
          <item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" />
        </el-menu-item>
      </app-link>
    </template>

    <el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
      <template slot="title">
        <item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />
      </template>
      <sidebar-item
        v-for="child in item.children"
        :key="child.path"
        :is-nest="true"
        :item="child"
        :base-path="resolvePath(child.path)"
        class="nest-menu"
      />
    </el-submenu>
  </div>
</template>

<script>
import path from 'path'
import { isExternal } from '@/utils/validate'
import Item from './Item'
import AppLink from './Link'
import FixiOSBug from './FixiOSBug'

export default {
  name: 'SidebarItem',
  components: { Item, AppLink },
  mixins: [FixiOSBug],
  props: {
    // route object
    item: {
      type: Object,
      required: true
    },
    isNest: {
      type: Boolean,
      default: false
    },
    basePath: {
      type: String,
      default: ''
    }
  },
  data() {
    // To fix https://github.com/PanJiaChen/vue-admin-template/issues/237
    // TODO: refactor with render function
    this.onlyOneChild = null
    return {}
  },
  methods: {
    hasOneShowingChild(children = [], parent) {
      const showingChildren = children.filter(item => {
        if (item.hidden) {
          return false
        } else {
          // Temp set(will be used if only has one showing child)
          this.onlyOneChild = item
          return true
        }
      })

      // When there is only one child router, the child router is displayed by default
      if (showingChildren.length === 1) {
        return true
      }

      // Show parent if there are no child router to display
      if (showingChildren.length === 0) {
        this.onlyOneChild = { ... parent, path: '', noShowingChildren: true }
        return true
      }

      return false
    },
    resolvePath(routePath) {
      if (isExternal(routePath)) {
        return routePath
      }
      if (isExternal(this.basePath)) {
        return this.basePath
      }
      return path.resolve(this.basePath, routePath)
    }
  }
}
</script>

注意:使用了递归组件,递归组件必须设置name

面包屑

面包屑导航是通过$route.matched数组动态生成的,面包屑是根据路由嵌套来生成的,所以设置路由的时候要根据业务去配置好。

<template>
  <el-breadcrumb class="app-breadcrumb" separator-class="el-icon-arrow-right">
    <transition-group name="breadcrumb">
      <el-breadcrumb-item :key="item.path" v-for="(item,index) in levelList">
        <span class="no-redirect" v-if="item.redirect==='noRedirect'||index==levelList.length-1">{{ $t('sidebar.'+item.meta.title) }}</span>
        <a @click.prevent="handleLink(item)" v-else>{{ $t('sidebar.'+item.meta.title) }}</a>
      </el-breadcrumb-item>
    </transition-group>
  </el-breadcrumb>
</template>

<script>
import pathToRegexp from 'path-to-regexp'

export default {
  data () {
    return {
      levelList: null
    }
  },
  watch: {
    $route: {
      handler (route) {
        this.getBreadcrumb()
      },
      immediate: true
    }
  },
  methods: {
    getBreadcrumb () {
      let matched = this.$route.matched.filter(
        item => item.meta && item.meta.title
      )
      const first = matched[0]
        
      // 这一部分根据自己的项目设置:我们的项目home页是不需要面包屑的,所以设置了breadcrumb: false 
      if (this.$route.path === '/home') {
        matched = [
          { path: '/', redirect: '/home', meta: { title: 'home', breadcrumb: false } }
        ].concat(matched)
      } else if (!this.isHome(first)) {
        // 根匹配只要不是home,就作为home下一级
        matched = [
          { path: '/', redirect: '/home', meta: { title: 'home' } }
        ].concat(matched)
      }

      this.levelList = matched.filter(
        item => item.meta && item.meta.title && item.meta.breadcrumb !== false
      )
    },
    isHome (route) {
      const name = route && route.name
      if (!name) {
        return false
      }
      return name.trim().toLocaleLowerCase() === 'home'.toLocaleLowerCase()
    },
    pathCompile (path) {
      // To solve this problem https://github.com/PanJiaChen/vue-element-admin/issues/561
      const { params } = this.$route
      var toPath = pathToRegexp.compile(path)
      return toPath(params)
    },
    handleLink (item) {
      const { redirect, path } = item
      if (redirect) {
        this.$router.push(redirect)
        return
      }
      this.$router.push(this.pathCompile(path))
    }
  }
}
</script>

<style lang="scss" scoped>
.app-breadcrumb.el-breadcrumb {
  display: inline-block;
  font-size: 14px;
  line-height: 36px;
  background-color: #eeeeee;
  margin-bottom: 20px;
  width: 100%;
  padding-left: 10px;
  .no-redirect {
    color: #97a8be;
    cursor: text;
  }
}
</style>

处理面包屑这里,最重要的一点就是:要将路由嵌套做好,路由嵌套做好了,后续的面包屑处理起来就非常容易了。另外,path-to-regexp处理path有点问题,就是如果path上跟了?tab=2这样的参数时,点击面包屑的时候会报错,或许是我没找到path-to-regexp库的正确使用方式,我自己重新进行了处理才解决这个问题的。

服务封装

数据交互流程:

api service => request=>local mock/esay-mock/server api

axios拦截器

  • 有时需要对请求头、响应进行统一处理
  • 请求不同数据源时url会变化,需要根据环境自动修改url
  • 可能出现的跨域问题
import axios from 'axios'
import store from '../store/'

import {
  Loading
} from 'element-ui'

let loading = null
const service = axios.create({
  baseURL: process.env.BASE_URL, // 请求地址
  timeout: 40000, // request timeout
  withCredentials: true
})

// request interceptor
service.interceptors.request.use(config => {
  // config.headers['Accept'] = 'application/vnd.sd.v2+json'
  config.params = {
    ...config.params,
    // 混入 多语言
    lang: store.getters.language
  }
  if (config.isDefaultLoading) {
    loading = Loading.service()
  }
  return config
}, error => {
  Promise.reject(error)
})

service.interceptors.response.use(
  response => {
    const res = response.data
    const { code } = res
    if (response.config.isDefaultLoading) {
      loading.close()
    }

    if (code !== 0) {
      // 未登录
      if (code === 401 || code === 403) {
        if (window.location.pathname !== '/th/login') {
          store.dispatch('user/logout').then(() => {
            location.reload()
          })
        }
      }
      // @params config.isExistCode
      // @doc 处理 服务端 特殊返回格式
      // @default false
      if (!response.config.isExistCode) {
        return Promise.reject(res)
      }
      return Promise.reject(res)
    } else {
      return res
    }
  },
  error => {
    return Promise.reject(error)
  }
)
export default service

BASE_URL是设置的环境变量,创建.env.development/.env.production文件
// base api
BASE_API = '/dev-api'

数据mock

数据模拟两种常见方式,本地mock和线上esay-mock

本地mock

本地mock:修改vue.config.js,给devServer添加相关代码

  • 安装
npm i body-parser -D

注意:post请求需要额外安装依赖:body-parser,用于解析post请求时的body中的参数

  • vue.config.js
const bodyParser = require("body-parser");

module.exports = {
  devServer: {
      before: app => {
          // 设置参数处理中间件,之后的每次请求都经过这两个中间件
          app.use(bodyParser.json()); //处理post参数
          app.use(
            bodyParser.urlencoded({
              // 处理url参数
              extended: true
            })
          );
          app.post("/th/api/login/api", (req, res) => {
            const { login_name,password } = req.body;
            if (login_name === "admin" && Number(password) === 123456) {
              res.json({
                code: 0,
                message:'登录成功',
                msg:'登录成功',
                data: login_name
              });
            } else {
              res.json({
                code: -1,
                message: "用户名或密码错误",
                msg:'用户名或密码错误'
              });
            }
          });
      }
    }
  }
}  

注意:本地mock了数据,服务端同时也提供了相关的接口,优先走本地的接口,服务端的接口是不请求的。 其实本地mock数据这种方式,是需要懂一些服务端代码的,而且前端自己mock数据工作量蛮大的,那咱们一起去看一下更优的方式mockjseasy-mock

mockjs

介绍

  • 数据类型丰富

支持生成随机的文本、数字、布尔值、日期、邮箱、链接、图片、颜色等。

  • 拦截 Ajax 请求

不需要修改既有代码,就可以拦截 Ajax 请求,返回模拟的响应数据。安全又便捷

API

  • Mock.mock(url, type, data)

使用

  • 安装
yarn add mockjs -D
  • 简单使用
import Mock from 'mockjs'

let dataSource = Mock.mock({
  'dataSource|5': [{
    'key|+1': 1,
    'mockTitle|1': ['哑巴', 'Butter-fly', '肆无忌惮', '摩天大楼', '初学者'],
    'mockContent|1': ['你翻译不了我的声响', '数码宝贝主题曲', '摩天大楼太稀有', '像海浪撞破了山丘'],
    'mockAction|1': ['下载', '试听', '喜欢']
  }]
})

export default dataSource

  • 以请求的方式mock数据
// mock/api.js
// api.js
import Mock from 'mockjs'

const url = {
  tableDataOne: 'http://20181024Mock.com/mode1/tableDataOne',
  tableDataTwo: 'http://20181024Mock.com/mode1/tableDataTwo',
  tableDataThi: 'http://20181024Mock.com/mode1/tableDataThi'
}
export default [
  Mock.mock(url.tableDataOne, {
    message: '获取成功',
    msg: '操作成功',
    code: 0,
    'data|5': [{
      'key|+1': 1,
      'mockTitle|1': ['哑巴', 'Butter-fly', '肆无忌惮', '摩天大楼', '初学者'],
      'mockContent|1': ['你翻译不了我的声响', '数码宝贝主题曲', '摩天大楼太稀有', '像海浪撞破了山丘'],
      'mockAction|1': ['下载', '试听', '喜欢']
    }]
  })
]


// 请求数据
import '@/mock/api.js'
import $axios from 'axios'
$axios.get('http://20181024Mock.com/mode1/tableDataOne').then(res => {
  if (res.status === 200) {
    let data = res.data.data
    console.log(data)
  }
})
  • 创建node服务
mock/mock-server.js
let express = require('express') // 引入express
let Mock = require('mockjs') // 引入mock

let app = express() // 实例化express

// 解决跨域问题
app.use(function (req, res, next) {
  res.header('Access-Control-Allow-Origin', '*')
  res.header('Access-Control-Allow-Methods', 'PUT, GET, POST, DELETE, OPTIONS')
  res.header('Access-Control-Allow-Headers', 'X-Requested-With')
  res.header('Access-Control-Allow-Headers', 'Content-Type')
  next()
})

app.use('/mode2/DataOne', function (req, res) {
  res.json(Mock.mock({
    'status': 200,
    'code': 0,
    'data|1-9': [{
      'key|+1': 1,
      'mockTitle|1': ['肆无忌惮'],
      'mockContent|1': ['角色精湛主题略荒诞', '理由太短 是让人不安', '疑信参半 却无比期盼', '你的惯犯 圆满', '别让纠缠 显得 孤单'],
      'mockAction|1': ['下载', '试听', '喜欢']
    }]
  }))
})

app.listen('3333', () => {
  console.log('监听端口 3333')
})

// 获取数据
$axios.get('http://localhost:3333/mode2/DataOne').then(res => {
  if (res.status === 200) {
    let data = res.data.data
    console.log(data)
  }
})

easy-mock

easy-mock实现了cors,所以咱们使用的时候,不用担心跨域的问题。

  • 准备工作
    • 安装nvm

      windows

      mac

      使用nvm安装node8.x:nvm install 8.16.0

    • 安装mongodb

      windows

      mac

    • 安装 redis

      windows

      mac

    • 克隆easy-mock项目

      git clone https://github.com/easy-mock/easy-mock.git
      
  • 步骤

因为easy-mock的官网不是太稳定,自己在本地搭建一个easy-mock的服务器

  1. 启动redis redis-server
  2. 启动mongodb mongod
  3. 切换node版本 nvm use 8.16
  4. 启动easy-mock npm run dev
  5. 登录easy-mock
  6. 创建项目
  7. 创建需要的接口
  8. 修改base_url,.env.development
VUE_APP_BASE_API = 'https://easy-mock.com/mock/5cdcc3fdde625c6ccadfd70c/kkb-
cart'
  1. 项目中直接调用相关接口即可

解决跨域

现在开发基本都是前后端分离的,接口服务器会与前端分离开来,所以经常情况下会造成跨域的问题。那么跨域到底是前端的问题还是后端的问题呢?所有的跨域问题一定是后端的问题,跟前端关系不大,但是前端是有办法解决的。

浏览器的问题引起的,只要发出去的请求和当前网站所在的地址不一样(包括:协议、端口号、子域名),会阻止请求发出去,有以下几种方式解决跨域问题:

  1. jsonp script的src,get请求(其实是躲避,没有真正解决)现在基本已经淘汰了
  2. cors options预处理(浏览器会发送2次请求)公共api一般用这种方式
  3. proxy nginx代理,不直接与api server打交道就不会跨域,服务器请求服务器是不存在跨域问题的

  • webpack中配置proxy代理

devServer:{
    port:port,
    proxy: {
      // 代理/th/api/login  到http://scm.ksher.cn/th/api/login
      [process.env.BASE_URL]: {
        target: "http://scm.ksher.cn",
        changeOrigin: true,
        pathRewrite:{
            // 重写之后:代理/th/api/login  到http://scm.ksher.cn/login
            ["^"+process.env.BASE_URL]:""
        }
      },
}

注意:如果接口本身没有/th/api,则需要通过pathRewrite来重写了地址

参考

手摸手,带你优雅的使用 icon