中大型Vue项目的前端架构-1

Posted 熬夜的小青年

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了中大型Vue项目的前端架构-1相关的知识,希望对你有一定的参考价值。

目录

 

前言

项目目录

Api后端接口

公共Helper

sevice 网络请求封装(axios封装)

utils工具类

main.js webpack入口文件

使用方法


前言

接触Vue已经有几年了,每次新建项目都有一些新奇的想法,之前分享过一篇Vue中使用Axios拦截器(拦截请求与相应),由于我最近的项目需要长期维护,所以不能再向小项目那种方式创建前端架构了,需求需要方便维护、高扩展性,但不是说我最初的那种方式不好,只是不适合中大型项目而已。所以今天我分享一下我在中大型项目中封装Axios和Helper的方法。每个人的需求不同,抱着学习的态度,大家一起分享经验。所以废话不多说,直接上干货。

项目目录

首先是我的项目架构,由于我的项目分的比较细,我只展示部分和本文章相关的模块。

|-----src 
|   |-----common 公共文件
|   |    |-----Api 后端接口
|   |    |    |-----modules 接口模块
|   |    |    |    |-----User.js 用户接口模块
|   |    |    |-----index.js 公共接口模块
|   |    |-----Helper 公共Helper
|   |    |    |-----index.js 公共Helper
|   |    |    |-----.....js 其他公共方法抽离
|   |-----sevice 网络请求封装(axios封装)
|   |    |-----api.js 请求方式封装
|   |    |-----request.js axios封装
|   |-----utils 工具类
|   |    |-----vue-install.js vue全局绑定工具类
|   |-----main.js webpack入口文件


Api后端接口

为什么要把接口单独抽离出模块呢?是因为在中大型项目中后端可能使用saas平台架构,拆分出不同模块,为了方便维护,快速查找定位接口,我们就需要将单文件进行解耦,抽离相同模块方便维护。当然如果后端没有用saas平台架构或接口较少的话,可以不使用这种方式直接写到index.js中这些都是根据个人需求去改变的无需纠结。

// index.js
import User from './modules/User'

export default {

  User, // 将独立的模块抽离到单独的js文件中可以方便统一维护

  // 将公共的接口抽离出来方便维护 method:请求方式 url:接口地址
  sendSms: { method: 'post', url: '' },

  uploadFile: { method: 'post', url: '' }
}
// User.js
export default {
    getUser: {method: 'get', url: ''}
}

公共Helper

同理,helper也可以抽离出不同的方法,方便维护。

// index.js
import deepClone from './deepClone'
import FormatDate from './FormatDate'

const Helper = {

    /**
     * 深度拷贝
     * @param {object} 对象
     * @return {object}
     */
    deepClone,

    /**
     * 日期格式化
     * @param {date} timestamp 日期/时间戳
     * @param {string} format 格式 默认值 Y-m-d
     * @return {string}
     */
    FormatDate,

    /**
     * 获取指定日期之间的所有日期
     * 日期格式 yyyy-MM-dd
     * @param {string} start 开始日期
     * @param {string} start 结束日期
     * @return {array}
     */
    getAllDate,

    /**
     * 随机字符串
     * @param {boolean} lower 小写字母
     * @param {boolean} upper 大写字母
     * @param {boolean} number 数字
     * @param {boolean} symbol 特殊字符
     * @param {boolean} length 长度
    */

    RandomString,

    /**
     * 计算请求分页参数
     * @param {object} route 当前页面路由
     * @return { limit: 10, page: 1, offset: 0 } limit = 长度, page = 页码, offset = 偏移量
     */
    getPerPage (query) {
        let limit = parseInt(query.limit) || 10,
            page = parseInt(query.page) || 1,
            offset = (page - 1) * limit;
        return {
            limit, page, offset
        }
    },

    /**
     * 清理对象
     * 去除属性值为 空(字符串), null, undefined
     * 转换值为数字,true,false的字符串为对应的数据类型
     * @param {object} obj 对象
     * @return {object}
     */
    clearObject (obj) {
        let o = {};
        for (const k in obj) {
            let v = obj[k];
            if (v === null || v === undefined) continue;
            // 非字符串
            if (toString.call(v) !== '[object String]') {
                o[k] = v;
                continue;
            }
            v = obj[k].trim();
            // 过滤空值
            if (v.length === 0) continue;

            // 正数,负数,浮点数
            if (/^(-?\\d+)(\\.\\d+)?$/.test(v)) {
                o[k] = Number(v);
            }
            // 布尔值
            else if (v === 'true' || v === 'false') {
                o[k] = (v === 'true');
            }
            // false
            else {
                o[k] = v;
            }
        }
        return o;
    }

}


export default Helper;

由于有些代码比较长我这里只分享一部分模块。

// 深度复制
const deepClone = (obj) => {
    let o;
    if (typeof obj === 'object') {
        if (obj === null) {
            o = null
        } else {
            // 数组
            if (obj instanceof Array) {
                o = [];
                for (const item of obj) {
                    o.push(deepClone(item))
                }
            }
            // 对象
            else {
                o = {};
                for (const j in obj) {
                    o[j] = deepClone(obj[j])
                }
            }
        }
    }
    else {
        o = obj;
    }
    return o;
}

export default deepClone;

到此我的common公共文件就完成了,其实中心思想还是将之前的单文件抽离出不同的模块,这样方便后面扩展。接下来是网络请求的封装。这里可能比较绕,不过仔细看过后还是能理解的。

sevice 网络请求封装(axios封装)

// api.js
import Api from '@/common/Api'
import Helper from '@/common/Helper'
import axios from './request'

const axiosApi = {
  // get请求
  createGet (url) {
    return (args = {}) => {
      return axios({
        method: 'GET',
        url: url,
        ...args
      })
    }
  },

  // post请求
  createPost (url) {
    return (args = {}) => {
      let parmas = ''
      /*
        不挂载到url地址上 为什么我这里要这么写呢,
        因为后端post接收参数有一部分是从地址栏获取,
        虽然我很诧异但是我直接给他两份他随便获取,
        当然可以根据notMountUrl参数选择不绑定到地址栏
        */ 
      if (!args.notMountUrl) {
        parmas = `?${Helper.formatParams(args.data)}`;
      }
      return axios({
        method: 'POST',
        url: `${url}${parmas}`,
        ...args
      })
    }
  },
// .... 当然你也可以写更多请求方式或者根据不同的需求调用不同的Axios封装

  // 创建API请求方法
  crateApi (apiConfig) {
    let methods = {}
    // 获取所有api的Key值进行循环
    Object.keys(apiConfig).forEach(key => {
      let item = apiConfig[key]

      // 子集请求 判断是否是单独模块 如果是就递归子集
      if (!item.method && !item.url) {
        return methods[key] = this.crateApi(item)
      }
      // 接口动态创建
      const method = item.method.toLocaleUpperCase()
      if (method === 'GET') {
        methods[key] = this.createGet(item.url)
      }
      else if (method === 'POST') {
        methods[key] = this.createPost(item.url)
      }
    })
    return methods
  }
}
// 这里一定是抛出创建方法,如果你晕了可以从这一步往回走,慢慢你就懂了
export default axiosApi.crateApi(Api)
// request.js
import axios from 'axios'
// 这里我使用的是vant ui库可根据需求更换不同的ui库
import { Toast } from 'vant';
import store from '@/store'
import router from '@/router'
// statusCode 错误状态码 这里错误码其实就是将网络请求中的所有status进行了键值对的封装方便调用
import statusCode from '@/common/BaseData/status_code.js'


// 是否为生产环境
const isProduction = process.env.NODE_ENV == "production";

// 创建一个axios实例
const service = axios.create({
  baseURL: !isProduction ? '/api' : process.env.VUE_APP_BASE_URL, // 前缀由于我使用的是webpack的全局变量,这里其实也可以写死
  withCredentials: true, // 当跨域请求时发送cookie
  timeout: 20000 // 请求超时时间
})

// 请求拦截器
service.interceptors.request.use(
  config => {
    // 获取token
    const hasToken = store.getters.Token

    // 设置 token设置token
    if (hasToken) {
      config.headers['token'] = hasToken
    }
    // .... 其他操作根据具体需求增加
    return config
  },
  error => {
    // 处理请求错误
    // console.log(error)
    return Promise.reject(error)
  }
)

// 响应拦截器
service.interceptors.response.use(

  /**
   * 通过自定义代码确定请求状态
   */
  response => {
    const res = response.data
    // 这里的error是后台返回给我的固定数据格式 可根据后端返回数据自行修改
    const { error } = res;
    // error不为null
    if (error) {

      // 弹出报错信息
      Toast.fail({
        icon: 'failure',
        message: error.msg
      })

      // 后端返回code的报错处理
      switch (error.code) {
        case '1000':
        case '1001':
        case '1002':
        case '1003':
          router.replace({ path: '/error', query: { code: error.code, msg: error.msg } })
          break;
        case '6000':
        case '6100':
          // 清空Token 重新登录
          store.dispatch('user/resetToken')
          return Promise.reject(new Error(error.msg));
        case '6200':
        case '7000':
        case '19000':
        default:
          // 如果状态码不是 则判断为报错信息
          return Promise.reject(new Error(error.msg))
      }
    } else {
      // 正常返回
      return res
    }

  },
  error => {
    // 这里就是status网络请求的报错处理 主要处理300+ 400+ 500+的状态
    console.error('err:' + error)
    // 弹出请求报错信息
    Toast(statusCode[error.statusCode])
    return Promise.reject(error)
  }
)
// 向外抛出
export default service

service网络请求封装结束,这里主要的是api.js的封装比较绕,其实就是动态创建后端接口的方法,万变不离其宗,都是换汤不换药的写法

utils工具类

最后的工具类其实就是将我们所有的方法绑定到vue的prototype原型上,以此形成完整的闭环 如果不了解vue绑定插件的机制的话,可以去vue.js官网自行查找,其实没多难。

// vue-install.js 全局绑定
import Api from '@/service/api'
import Helper from '@/common/Helper'

export default {
  install: (Vue) => {
    // 全局注入 $api 请求 将我们api.js抛出的方法绑定到vue实例上
    Vue.prototype.$api = Api
    // 全局注入 $helper 辅助方法 同理将helper公共方法绑定到vue上
    Vue.prototype.$helper = Helper
  }
}

工具类的绑定结束,致辞我们就差最后一步就完成我们的闭环了,那就是引入到main.js中

main.js webpack入口文件


import Vue from 'vue'

import router from './router'
import store from './store'
// .... 其他依赖引入
import vueInstall from './utils/vue-install' // 全局注册


// 全局绑定 直接使用use方法绑定
Vue.use(vueInstall);

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

使用方法

// 某页面
async created () {

    // 请求User模块
    const res = await this.$api.User.getUser({ 
    data: { 
        // 参数.....
    },
    // 选择是否挂载到url上 
    notMountUrl: false
        
    })
    // 深拷贝
    const newResult = this.$helper.deepClone(this.result)
  }

大功告成,这就是目前我项目中网络请求与helper公共方法的封装。希望对大家有所帮助,同时也虚心接受大家的各种建议和意见。希望大家在评论区进行评论。我会持续对我所掌握的知识进行分享。不忘初心坚持开源开放的学习态度。

以上是关于中大型Vue项目的前端架构-1的主要内容,如果未能解决你的问题,请参考以下文章

基于Vue的前端架构,我做了这15点

vue的前端目录结构

阿里架构师:浅谈大型项目前端架构设计

搭建基于Vue的前端架构,总结15点经验(条理清晰,内容详实,系统全面,细节到位)

vue.js学习笔记1

Vue2 Vuex在大型项目中的应用