# 新增模块

项目搭建了一个开发的框架,在此基础上,大家可以快速的添加组件的业务模块。涉及到前端的路由、view、api、组件和样式。比如我们需要新增一个博客的功能模块,下面将针对每个方面进行详细的介绍。

# 路由

新增路由的方式有两种,第一种是在 src/router/index.js 中增加你需要添加的路由。

如:新增一个博客的文章列表页面

{
  path: '/blog',
  component: Layout,
  redirect: '/blog/article',
  name: 'blog',
  meta: {
    title: '博客管理',
    icon: 'clipboard'
  }
}

说明

仅仅这样是无法看到列表菜单项的,它只是创建了一个基于layout的一级路由--博客管理模块,你还需要在它下面的 children 中添加子路由。

{
  path: '/blog',
  component: Layout,
  redirect: '/blog/article',
  name: 'blog',
  meta: {
    title: '博客管理',
    icon: 'clipboard'
  },
  children: [
    {
      path: 'article',
      component: ()=>import('blog/article'),
      name: 'ArticleList',
      meta: { 
        title: '文章列表' 
    	icon: "list"
      }
    }
  ]
}

这样侧边栏就会出现如图所示的 menu-item 了。

image-20210107110827242

第二种是直接由后台统一配置,也是作者推荐的一种方式,后台系统可以设置侧边栏菜单,前端项目不用做任何编码,由系统或自动获取后端的菜单配置,动态生成如上一样的路由信息。

image-20210107111114667

我们可以看一下后台返回的具体菜单格式:

image-20210107111348706

# View

经过以上的步骤,我们前端项目已经有了博客模块所有的路由信息,这时候在浏览器输入路由地址或者点击左侧侧边栏菜单,系统会报错,因为找不到路由对应的视图,因此接下来的步骤很重要,我们需要定义每个功能模块的视图。

按照约定,所有的视图存放的路径为:src/views,所以我们在此目录下新建一个文件夹 blog ,前面定义组件的路径是component: ()=>import('blog/article'),因此需要再建一个子文件夹article,对应的视图文件index.vue

image-20210107113052147

定义完视图,访问侧边栏菜单,就可以看到内容区域就是该视图index.vue里面的内容,我们可以编辑该文件,来完成业务模块视图。当然,博客模块还有新增,编辑、查看详情页等等功能,我们同样按照上面的方法,新增视图文件,并在路由配置中定义相应的路由信息。

# Api

视图页面需要访问后端提供的API接口,为了使用方便,首先新增模块定义接口的js文件src/api/user.js

import request from '@/utils/request'

export function login(data) {
  return request({
    url: '/api/auth/login',
    method: 'post',
    data
  })
}

export function getInfo() {
  return request({
    url: '/api/users/info',
    method: 'get'
  })
}

export function logout() {
  return request({
    url: '/api/auth/logout',
    method: 'delete'
  })
}

export function captcha() {
  return request({
    url: '/api/auth/captcha',
    method: 'get'
  })
}

src/utils/request框架中添加了全局请求/响应拦截器,对于后端API的调用和接口返回结果的处理进行了统一的封装:

import axios from 'axios'
import qs from 'qs'
import { Message, Notification } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'
import Cookies from 'js-cookie'

// create an axios instance
const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_API // url = base url + request url
  // withCredentials: true, // send cookies when cross-domain requests
  // timeout: 5000 // request timeout
})

// request interceptor
service.interceptors.request.use(
  config => {
    // do something before request is sent

    if (store.getters.token) {
      // let each request carry token
      // ['X-Token'] is a custom headers key
      // please modify it according to the actual situation
      config.headers['Authorization'] = getToken()
    }

    // 如果是get请求的数组参数,序列化转换:p=[a,b,c] => p=a,b,c
    if (config.method === 'get') {
      config.paramsSerializer = function(params) {
        return qs.stringify(params, { indices: false })
      }
    }
    return config
  },
  error => {
    // do something with request error
    console.log(error) // for debug
    return Promise.reject(error)
  }
)

// response interceptor
service.interceptors.response.use(
  /**
   * If you want to get http information such as headers or status
   * Please return  response => response
  */

  /**
   * Determine the request status by custom code
   * Here is just an example
   * You can also judge the status by HTTP Status Code
   */
  response => {
    const res = response.data
    if (Object.prototype.hasOwnProperty.call(res, 'code')) {
      if (res.code !== 0) {
        Message({
          message: res.msg || 'Error',
          type: 'error',
          duration: 5 * 1000
        })
        return Promise.reject(new Error(res.msg || 'Error'))
      } else {
        return Promise.resolve(res.data)
      }
    } else {
      return Promise.resolve(res)
    }
  },
  error => {
    console.log('err' + error) // for debug
    const res = error.response
    if (!res) Notification.error('无法从后台服务器返回数据,请联系管理员!')
    if (res.status === 401 || res.status === 403) {
      // 重登录
      store.dispatch('user/logout').then(() => {
        // 用户登录界面提示
        Cookies.set('SessionExpired', true)
        location.reload()
      })
    } else if (res.status === 404) {
      Message({
        message: '后台服务器不存在此接口,请联系管理员!',
        type: 'error',
        duration: 5 * 1000
      })
    } else {
      Message({
        message: res.data.message || '未知错误,请联系管理员!',
        type: 'error',
        duration: 5 * 1000
      })
    }
    return Promise.reject(error)
  }
)

export default service

在需要的地方添加引用,并通过声明的实例来调用后端API方法:

import { captcha } from '@/api/user'
...
 methods: {
    refreshCaptcha() {
      captcha().then((res) => {
        this.codeUrl = res.img
        this.loginForm.uuid = res.uuid
      })
    }
 }

# 组件

通常来说,一些全局使用的组件,定义在 src/components ,如上传组件、分页组件等等能被公用的组件。每个单独页面或者模块特定业务组件建议保存在当前业务目录下,并创建一个 components 目录专门保存这些组件。如:src/views/profile/components/xxx.vue,这样拆分大大减轻了维护成本。

在需要使用组件的地方添加引用如下:

<template>
  <div class="app-container">
    ...
    <updatePass ref="pass" />
    ...
  </div>
</template>

import updatePass from './components/updatePass'
...
export default {
  name: 'Profile',
  components: { updatePass },
}

# 样式

页面的样式同样也区分为全局样式和本地样式, src/style 放置一下全局公用的样式,每一个页面的样式就写在当前 views下面,请记住加上scoped 或者命名空间,避免造成全局的样式污染。

<style rel="stylesheet/scss" lang="scss" scoped>
.search-result {
  margin-bottom: 30px;
}
.search-result .search-title {
  font-size: 16px;
  color: #007bff;
  text-decoration: underline;
}
.search-result .search-snapshot {
  font-size: 14px;
}
.el-pagination {
  text-align: center;
}
</style>