从0到1基于vue搭建后台管理项目

本文主要内容是如何从0到1基于vue搭建一个后台管理项目的前端工程。示例的代码地址:https://github.com/tyuqing/vue-admin

1 创建项目

使用vue-cli3快速创建项目,具体的创建方法vue-cli官网上有说明。创建时大家可根据自己项目的需求选择合适的模块,我使用的配置如下图所示。
从0到1基于vue搭建后台管理项目_第1张图片
其中选中了Babel, Router, Vuex, CSS Pre-processors, Linter, Unit。 CSS Pre-processors选的是sass,eslint的规则选的是Airbnb,并且只在commit时校验并修复。这样一个基本的vue项目就创建成功了。
此时目录结构如下:
从0到1基于vue搭建后台管理项目_第2张图片
此时,可引入基础组件库,我们的基础组件库使用的是iview。大家也可以使用饿了么的elementUI

2 ESlint

eslint的规范有:airbnb、standard和prettier等。前面有提到过我们使用的eslint规则是airbnb。
eslint的自动修复方式有:

  • 每次保存时lint代码。该方式是通过 eslint-loader实现的。
  • git commit时lint代码
    git commit时lint代码是在通过git钩子在commit时调用lint的命令(即下面配置中的lint-stagedlint-staged执行的命令为vue-cli-service lint)来完成eslint的检查和修复,主要配置是在package.json中:
  // package.json
  "gitHooks": {
    "pre-commit": "lint-staged"
  },
  "lint-staged": {
    "*.{js,vue}": [
      "vue-cli-service lint",
      "git add"
    ]
  }

以上配置示例是通过vue cli3搭建的项目的eslint自动检查和修复配置,主要是使用“yorkie”和“lint-staged”这两个npm包来实现的。(其中yorkie在@vue/cli-service中有引用,所以项目中引用@vue/cli-service即可),大家还可以通过“husky”和“lint-staged”来实现。

3 接口层

我们的接口请求的http库使用的axios。一般在业务上,当接口返回数据异常时,不同的异常前端需要通过不同的方式进行提示,如Message或Modal方式的提示。所以我们需要结合业务对axios进行封装,对异常进行统一处理,若分散到各个组件将会使代码变得冗余和难以维护。

3.1 axios封装

我们接口的响应体统一结构如下

{"code":200,"msg":"OK","data":{}}

我们是通过判断响应体中code来判断是否异常的,新建文件src/api/base.js对axios进行封装,代码如下:

// src/api/base.js
import axios from 'axios';
import { Message, Modal } from 'iview';

const request = axios.create({
  // 统一设置超时时间
  timeout: 20000,
  // 返回数据类型
  responseType: 'json',
});

/**
 * 接口异常的统一处理 判断响应体中的code进行不同的异常提示
 */
request.interceptors.response.use(
  (response) => {
    const res = response.data;
    if (res.code === 200) {
      // 正确时的处理
      return res;
    } else if (/* 条件 */) {
      Message.error(res.message); // Message方式的提示
    } else if (/* 条件 */) {
      Modal.error({ title: '提示', content: res.message }); // modal方式的提示
    }
    return Promise.reject(new Error(res.message || 'Error'));
  },
  (error) => {
    // 请求异常时的处理
    if (error.message) {
      Message.error(error.message);
    }
    return Promise.reject(error);
  },
);
export default request;

对axios的封装中,我们首先传入了一些简单的配置创建了一个axios的实例(request);然后,调用了axios的API(interceptors.response.use)对请求的响应进行了统一拦截,通过判断响应体中的code进行不同的处理:

  • 正常时
    正常时返回res(响应体),这里我们没有返回变量response,因为response中包含了http状态等一系列信息,在base.js已针对这些信息进行了统一处理,而在组件或页面中并不需要这些信息,所以请求正常时直接返回响应体
  • 响应体异常
    响应体中code异常时,不同的code通过不同的方式进行异常提示,后返回一个rejected Promise
  • HTTP状态码异常
    当HTTP状态码异常等,提示后返回一个rejected Promise
    封装后,一次接口请求的时序图如下所示
    从0到1基于vue搭建后台管理项目_第3张图片

3.2 接口的定义

以获取用户信息的接口为例,文件src/api/user.js中引入base.js中的request,然后提供getUserInfo方法

/* src/api/user.js */
import request from './base.js';
export function getUserInfo(params) {
  return request.get('/api/user/info', { params });
}

3.3 接口的使用

前面已经提到过,接口成功会返回响应体,失败被抛出一个Error,所以在组件中调用接口时,直接在then进行业务成功的逻辑,无需进行是否成功的判断,这样代码逻辑上会清晰很多。以在组件中使用获取用户信息接口为例示例如下。接口异常在前面提到的base.js已经统一处理了,所以catch和finally相对会使用的比较少。

<script>
import { getUserInfo } from '@/api/user';
export default {
  //......
  methods: {
    getUserInfo() {
      getUserInfo()
        .then(() => {
          // 成功后的处理
        })
        .catch(() => {
          // 异常后的处理
        })
        .finally(function() {
          // promise完成后的处理
        });
    },
  },
};
</script>

3.4 接口的mock和代理

在本地启动工程开发时,若后端接口未开发好,接口可以走本地的mock配置文件;若后端接口开发好了,接口可以代理到测试环境上。其中mock的功能可通过插件http-mockjs来实现,github地址:https://github.com/brizer/http-mocker

4 全局变量

使用vuex来进行全局变量共享的管理。以用户信息为例,讲解全局变量的存储。新建文件src/store/modules/user.js,用于获取并存储用户信息,代码示例如下:

// src/store/modules/user.js
import { getInfo } from '@/api/user';
import router, { resetRouter } from '@/router';

const state = {
  role: '',
  email: '',
  // 其他用户信息...
};

const mutations = {
  SET_ROLE: (state, role) => {
    state.role = role;
  },
  SET_EMAIL: (state, email) => {
    state.email = email;
  },
  // 其他用户信息...
};

const actions = {
  // 获取用户信息
  getInfo({ commit }) {
    return getInfo()
      .then(response => {
        const { data } = response;
        const { role, email } = data;

        commit('SET_ROLE', role);
        commit('SET_EMAIL', email);
        // 其他用户信息...
        return data;
      })
      .catch(error => {
        reject(error);
      });
  },
};

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

因为用户信息在工程中可能经常被使用,可以将用户信息配置在getter中,步骤如下,新建文件src/store/getters.js,使任何组件能更快速方便的访问到用户信息。

// src/store/getters.js,
const getters = {
  role: state => state.user.role,
  email: state => state.user.email,
  // 其他用户信息...
};
export default getters;

将使用vue-cli3生成的src/store.js文件,改为src/store/index.js,如下所示,使全局变量分模块进行管理

// src/store/index.js
import Vue from 'vue';
import Vuex from 'vuex';
import getters from './getters';

Vue.use(Vuex);

const modulesFiles = require.context('./modules', true, /\.js$/);

// 不需要 `import app from './modules/app'`
// 自动载入文件夹./modules中的内容
const modules = modulesFiles.keys().reduce((modules, modulePath) => {
  // set './app.js' => 'app'
  const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1');
  const value = modulesFiles(modulePath);
  modules[moduleName] = value.default;
  return modules;
}, {});

const store = new Vuex.Store({
  modules,
  getters,
});

export default store;

该文件会自动加载src/store/modules/文件夹下的所有模块,即变量modules,在新建了模块后无需再手动引入。
在组件中调用store的模块的action的示例如下:

export default {
    methods: {
        demo() {
            this.$store.dispatch('product/getInfo', productId); // 其中product为模块
        }
    }
}

在组件中使用store的getter的数据的示例如下:

import { mapGetters } from 'vuex';
export default {
    computed: {
      ...mapGetters(['email']) // 即前面提到的用户信息的email
    }
}

在组件中使用store的模块的数据的示例如下:

import { mapState } from 'vuex';
export default {
    computed: {
    ...mapState({
      productInfo: state => state.product.productInfo, // 其中product为模块
    }),
}

5 页面

5.1 布局

通常后台管理系统的布局如下图所示,包含菜单栏、工具栏和主体内容等。其中,左侧菜单栏和顶部工具栏是固定不变的,于是我们需要一个包含上述两部分内容的公用的布局模块,然后主体内容根据路由的变化而变化。
从0到1基于vue搭建后台管理项目_第4张图片
新建如下图所示中红框中的文件

其中src/layout/index.vue是一个左-上下结构布局入口组件,代码如下:

// src/layout/index.vue
<template>
  <div class="g-wrap">
    <Sidebar class="g-left" />
    <div class="g-right">
      <div class="g-right_top">
        <Navbar />
      div>
        <section class="g-app-main">
          <router-view />
        section>
    div>
  div>
template>

<script>
import { Navbar, Sidebar } from './components/index';

export default {
  name: 'Layout',
  components: { Navbar, Sidebar },
  computed: {},
  methods: {},
};
script>

5.2 侧边菜单栏

后台管理系统会需要在侧边展示导航栏,其中的菜单列表我们可以根据src/router/index.js(路由配置文件)中配置的路由来生成,这样就省去了还要写一遍菜单列表配置的麻烦。

5.3 面包屑

一般我们会需要展示网站的层级结构,告知用户当前所在位置,这时,我们会用到面包屑组件。同样,该层级我们可以根据路由对象的属性来生成。路由对象属性$route.matched包含当前路由的所有嵌套路径片段的路由记录,可以基于该数组进行处理得到需要展示的层级结构

6 路由

路由可能会有访问权限,所以路由表分为两个:一个是存放不需要权限的公共页面,如登录页、首页等;一个是存放需要权限的页面,如通过用户角色判断或需要登录等。前者在实例化vue时就挂载,后者在请求到用户角色后再通过router.addRoutes方法来动态挂载。

6.1 常量路由

无需权限的常量路由表示例如下:

//  src/router/index.js
import Vue from 'vue';
import Router from 'vue-router';
import Layout from '@/layout/index.vue';

Vue.use(Router);
// 无权限校验的路由
export const constantRoutes = [
  {
    path: '/login',
    component: () => import('@/views/login/index.vue'),
    hidden: true,
  },
  {
    path: '/',
    component: Layout,
    redirect: '/dashboard',
    children: [
      {
        path: 'dashboard',
        component: () => import('@/views/dashboard/index.vue'),
        name: 'dashboard',
        meta: { title: '首页' },
      }
    ],
  }
]
export default new Router({
  routes: constantRoutes
});

6.2 动态路由

给需要权限的动态路由的meta标签配置roles属性,角色的校验是通过router.beforeEach方法在路由变更时,首次获取了用户信息后,判断路由的meta.roles来校验访问的权限,若没有配置meta.roles则不需要校验角色
示例如下:

// src/router/modules/demo.js
import Layout from '@/layout/index.vue';

const demoRouter = {
  path: '/demo',
  component: Layout,
  redirect: '/demo/list',
  name: 'demo',
  meta: { title: '示例列表' },
  hidden: true,
  children: [
    {
      path: 'list',
      component: () => import('@/views/demo/list.vue'),
      name: 'demo-list',
      meta: { title: '示例列表', breadcrumb: false },
    },
    {
      path: 'edit/:id',
      component: () => import('@/views/demo/edit.vue'),
      name: 'demo-edit',
      meta: { title: '示例编辑', activeMenu: 'demo-list', roles: ['admin', 'editor'] },
      hidden: true,
    },
  ],
};
export default demoRouter;
// src/router/index.js
import demoRouter from './modules/demo';
export const asyncRouterMap = [ demoRouter ];

注意事项:404页面要放在最后加载,否则所有页面都会跳转到404。

7 权限的校验

权限的校验分为

  • 校验用户是否登录
  • 校验用户角色有权访问的页面。
    角色的校验和动态路由的挂载是写在项目的入口文件main.js中的,也可以提成单独的文件在main.js中引用。以下为示例:
// main.js
import store from '@/store/index';
const WHITE_LIST = ['/login'];
router.beforeEach((to, from, next) => {
  if (WHITE_LIST.indexOf(to.path) !== -1) {
    // 白名单页面不需要登录
    next();
  } else {
    if (!store.getters.role) { // 判断是否获取了用户信息
      store.dispatch('user/getInfo').then(response => { // 获取用户信息
        const role = response.data.role;
        // 生成可访问的路由表
        const accessRoutes = generateRoutes(role)
        // 动态添加可访问路由表
        router.addRoutes(accessRoutes) 
        next({ ...to, replace: true })
      }).catch(() => {
        next(`/login`)
      });
    } else {
      next() //当已经登录过,说明已经通过角色生成了可访问的路由表,若无权限会跳转到404页面
    }
  }
});

在每次路由变更时执行的逻辑如下图所示:
从0到1基于vue搭建后台管理项目_第5张图片

流程如下:

  1. 判断目标路由是否在权限白名单中(如是否访问的是登录页),在则next();不在则执行步骤2
  2. 判断是否已经获取过用户信息 获取过则next();未获取过则执行步骤3
  3. 调用vuex的action(该action是调用接口)获取用户信息 后通过用户信息动态加载剩余路由。然后next({ ...to, replace: true })这样会使该路由变更的逻辑重新执行一遍,以确保所有路由加载成功。

未登录跳转到登录页的功能写在前面所提到的“axios封装”中。这样确保每个接口被调用时,若未登录都会跳转到登录页。并且在权限校验的方法中,首次进入时会调用用户信息接口这样来确认是否登录。

8 总结

最后搭建出项目的整体架构如下图所示:
从0到1基于vue搭建后台管理项目_第6张图片

原文地址

你可能感兴趣的:(vue)