Skip to content

基于vue搭建前端工程 #12

@tyuqing

Description

@tyuqing

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

1 创建项目

使用vue-cli3快速创建项目,具体的创建方法vue-cli官网上有说明。创建时大家可根据自己项目的需求选择合适的模块,我使用的配置如下图所示。
1
其中选中了Babel, Router, Vuex, CSS Pre-processors, Linter, Unit。 CSS Pre-processors选的是sass,eslint的规则选的是Airbnb,并且只在commit时校验并修复。这样一个基本的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
    封装后,一次接口请求的时序图如下所示
    api-base

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 布局

通常后台管理系统的布局如下图所示,包含菜单栏、工具栏和主体内容等。其中,左侧菜单栏和顶部工具栏是固定不变的,于是我们需要一个包含上述两部分内容的公用的布局模块,然后主体内容根据路由的变化而变化。
3
新建如下图所示中红框中的文件
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页面
    }
  }
});

在每次路由变更时执行的逻辑如下图所示:
permission

流程如下:

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

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

8 总结

最后搭建出项目的整体架构如下图所示:
编组

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions