VUE代码架构浅谈

VUE目录架构概述


- mock                    mock可以不需要后台,自动拦截ajax返回测试数据
 
- public                  公共目录
 
- src   
 
    api                   用于存放网络请求文件的目录
    
        index.ts          
   
        xxx目录            
    assets                存放静态文件的目录
    components            存放自定义组件的目录
    filter                过滤器的使用(例如时间data格式化)
        index.ts
    icons                 图标库引入
        svg  
        index.ts
    lang                  语言包引入(用于项目中多语言切换)
        en.js
        zh.js
    layout                每个页面的框架
    router                路由设置 
        index.ts
        modules           可以减少router下index整体的大小
    store                 vuex的目录
        modules           相应的文件存放
        getters           获取vuex变量get方式
        index.ts 
    utils                 工具目录
    views                 页面目录
    App.vue 
    main.ts  
    permisson.ts          权限文件
- .env.development        本地的环境变量
 
- .env.production         线上的环境变量
 
- vue.config.js           配置整体的vue项目
 
- package.json            所有下载的依赖统一管理文件
 
- README.md               项目介绍文档(一般上传源码管理器使用)

第一步、程序运行入口

VUE运行三件套

一、代码入口

main.js 真个VUE前端代码的入口,目的是初始化一个VUE实例,在实例中用render函数,将App.VUE组件渲染成一个DOM节点(视图);

render: h => h(App) //等价于render  (h) { return h(App); }

h 函数就是createElement,是 Vue.js 里面的 函数,这个函数的作用就是生成一个 VNode节点,render 函数得到这个 VNode 节点之后,返回给 Vue.js 的 mount 函数,渲染成真实 DOM 节点,并挂载到根节点上。

注:createElement 函数是用来生成 HTML DOM 元素的,也就是上文中的 generate HTML structures,也就是 Hyperscript,这样作者才把 createElement 简写成 h。

二、App.VUE

app.vue是vue页面资源的首加载项,是主组件,页面入口文件 ,所有页面都是在App.vue下进行切换的。也是整个项目的关键,app.vue负责构建定义及页面组件归集

归集的概念可以理解成一个根节点、一个载体,后续所有的页面都需要这个载体来承载,如下图所示:

三、permission.js

文件位置在项目目录/src/permission.js文件里,文件的功能用于路由鉴权计算(个人理解:通过业务逻辑实现路由调配)。

鉴权流程如下:

permission.js 鉴权计算业务逻辑实现(主要在router.beforeEach这个方法中)如下:

// 引入vue
import router from './router'
// 引入vuex仓库
import store from './store'
// 引入element-ui的提示组件
import { Message } from 'element-ui'
// 引入进度条、进度条风格
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
// 引入从cookie中获取 Token工具,登录后,token在cookie中
import { getToken } from '@/utils/auth' 
//  引入从 获取title工具,用于切换的时候,titile变化
import getPageTitle from '@/utils/get-page-title'
// 配置进度条
NProgress.configure({ showSpinner: false }) 
// 配置白名单:登录页面(不需要登录即可访问)
const whiteList = ['/login', '/auth-redirect'] 
// 全局前置路由守卫,核心逻辑都在守卫中
router.beforeEach(async(to, from, next) => {
  // 开始进度条
  NProgress.start()

  //设置页面标题
  document.title = getPageTitle(to.meta.title)

  // 从cookie中获取token
  const hasToken = getToken()
  // 如果有token 说明已经登录了
  if (hasToken) {
    if (to.path === '/login') {
      // 如果已经登录,又访问了登录页
      next({ path: '/' })//定向放行到后台首页面
      NProgress.done() //进度条结束
    } else {//你登录了,你去的除了登录页之外的页面
      // 在vuex中的仓库store中看你的权限(存在,且不为空)
      const hasRoles = store.getters.roles && store.getters.roles.length > 0
      if (hasRoles) { //权限roles存在,且不为空
        next()   //放行
      } else { //如果没有角色信息,比如刷新了浏览器
        try {
           //用vuex发起获取用户信息请求
          const { roles } = await store.dispatch('user/getInfo')

          // 用获取到的用户信息中的权限,传入VUEX方法,生成路由规则
          const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
          // console.log("accessRoutes",accessRoutes)
          // 将生成的当前用户的路由规则,添加到路由器,addRoutes方法,在user.js中
          router.addRoutes(accessRoutes)

          // 放行
          next({ ...to, replace: true })
        } catch (error) {
          // 获取权限过程异常,就清除登录状态
          await store.dispatch('user/resetToken')
          // 提示错误
          Message.error(error || 'Has Error')
          // 跳转到登录页面
          next(`/login?redirect=${to.path}`)
          NProgress.done()//进度条结束
        }
      }
    }
  } else {//如果没有token
     
      // 你访问的路径是不是白名单路径
    if (whiteList.indexOf(to.path) !== -1) {
      // 是白名单,就放行
      next()
    } else {
      // 如果不在白名单,就跳转登录页
      next(`/login?redirect=${to.path}`)
      NProgress.done()//进度条结束
    }
  }
})
// 全局后置路由守卫,就干了一件事,结束进度条
router.afterEach(() => {
  // finish progress bar
  NProgress.done()
})

const accessRoutes = await store.dispatch('permission/generateRoutes', data) //作用是根据角色信息,重新调配router/index.js内部的动态路由。

router/index.js 实现动态路由部分,重定向实际路由

第二步、编码进行时

一、layout页面布局

layout布局页面会在router/index.js中进行加载,是一种将页面布局抽象为可复用组件的方式,以便在多个页面之间共享布局逻辑。可通过修改layout不同的组件实现公共框架的客制化

二、store仓库

store会有以下几种属性

可在store/modules目录下创建***.js文件,文件定义并实现需要存储的数据信息,user.js代码如下:

import { login, logout, get_user_info } from '@/api/user'
import { getToken, setToken, removeToken } from '@/utils/auth'
import router, { resetRouter } from '@/router'

const state = { //定义要store的数据接口及,数据初始化
  token: getToken(),
  name: '',
  avatar: '',
  introduction: '',
  roles: '',
  language: 'zh-tw',
  status: '',
}

const mutations = {   //定义state数据的修改操作
  SET_TOKEN: (state, token) => {
    state.token = token
  },
  SET_INTRODUCTION: (state, introduction) => {
    state.introduction = introduction
  },
  SET_NAME: (state, name) => {
    state.name = name
  },
  SET_AVATAR: (state, avatar) => {
    state.avatar = avatar
  },
  SET_ROLES: (state, roles) => {
    state.roles = roles
  },
  SET_LANGUAGE: (state, language) => {
    state.language = language
  },
  SET_STATUS: (state, status) => {
    state.status = status
  },
}

const actions = {   //提交状态,调用mutations 方法对数据进行操作
  // user login
  login({ commit }, userInfo) {  //store释放出来的方法,供登录界面时调用,与下面login({ username:.....不是一个概念
    const { username, password } = userInfo  //userInfo 前端登录界面输入的参数
    return new Promise((resolve, reject) => {
      login({ username: username.trim(), password: password }).then(response => { //调用的API接口
        const { data } = response
        commit('SET_TOKEN', data.token) //调用mutations 方法 将token存在state数据结构中,页面刷新state数据结构会清空
        localStorage.setItem('language', data.lang) //调用浏览器缓存方法,进行数据暂存,刷新页面数据扔保留
        localStorage.setItem('roles', data.role)
        localStorage.setItem('status', data.status)
        localStorage.setItem('avatar', data.avatar)
        setToken(data.token)
        resolve()
      }).catch(error => {
        reject(error)
      })
    })
  },

  setStatus({ commit }, status) {
    localStorage.setItem('status', status)
    commit('SET_STATUS', status)
  },

  // get user info
  getInfo({ commit, state }) {  //暂存数据信息方法,至于如何调用这些方法,下面会介绍
    return new Promise((resolve, reject) => {
      get_user_info().then(response => {
        var data = {
          lang : response.data.lang,
          roles : response.data.role+'',
          status : response.data.status+''
        }
        commit('SET_LANGUAGE', data.language)
        commit('SET_ROLES', data.roles)
        commit('SET_STATUS', data.status)
        resolve(data)
      }).catch(error => {
        reject(error)
      })
    })
  },

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

将store的数据暴露出来共其他地方使用

getter.js文件,目的是获取state中的数据,但是不会改变数据

const getters = {
  sidebar: state => state.app.sidebar,
  size: state => state.app.size,
  device: state => state.app.device,
  visitedViews: state => state.tagsView.visitedViews,
  cachedViews: state => state.tagsView.cachedViews,
  token: state => state.user.token,
  avatar: state => state.user.avatar,
  name: state => state.user.name,
  introduction: state => state.user.introduction,
  roles: state => state.user.roles,
  permission_routes: state => state.permission.routes,
  errorLogs: state => state.errorLog.logs,
  language: state => state.user.language,
  status: state => state.user.status,
}
export default getters

index.js文件定义vuex.store

import Vue from 'vue'
import Vuex from 'vuex'
import getters from './getters'

Vue.use(Vuex)

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

const modules = modulesFiles.keys().reduce((modules, modulePath) => {
  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

如果要使用暂存数据则需在页面中加入下面代码:

import { mapGetters } from 'vuex'

computed: {
    ...mapGetters([
      'sidebar',
      'avatar',
      'device',
      'roles',
      'status', 
    ])
  },

//使用时直接this.status 即可拿到status的值

调用store的暂存数据的方法:

this.$store.dispatch('user/login', this.loginForm) //调用store暴露出来的login方法

 const data = await store.dispatch('user/getInfo') //调用store暴露出来的getInfo方法

三、utils拦截器

可以自定义一些方法,然后再页面中加载调用

//如utils/auth.js文件中定义
import Cookies from 'js-cookie'

const TokenKey = 'Admin-Token'

export function getToken() {
  return Cookies.get(TokenKey)
}

export function setToken(token) {
  return Cookies.set(TokenKey, token)
}

export function removeToken() {
  return Cookies.remove(TokenKey)
}

//其他文件调用
import { getToken, setToken, removeToken } from '@/utils/auth'

request 拦截器文件

在API接口文件中加入 import request from '@/utils/request' ,实现请求返回数据拦截

request.js代码实现:

import axios from 'axios'
import { MessageBox, Message } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'

const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
  timeout: 10000 // request timeout
})

service.interceptors.request.use(
  config => {
    if (window.maQuickConfig.baseURL) {
      config.baseURL = window.maQuickConfig.baseURL
    }
    config.headers['Content-Type'] = 'application/json;charset=UTF-8'
    if (store.getters.token) {
      config.headers['Authorization'] = getToken()
      config.headers['cdp-language'] = store.getters.language || 'zh-tw'
    }
    return config
  },
  error => {
    console.log(error) // for debug
    return Promise.reject(error)
  }
)

service.interceptors.response.use(
  response => { //处理请求返回数据,针对不同的报错信息,进行不同的处理机制
    const res = response.data

    if (res.code !== 0) {  //response信息 code如果不为0(即有报错)则进行如下处理
      Message({
        message: res.msg || 'Error',
        type: 'error',
        duration: 5 * 1000
      })

      if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
        MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', {
          confirmButtonText: 'Re-Login',
          cancelButtonText: 'Cancel',
          type: 'warning'
        }).then(() => {
          store.dispatch('user/resetToken').then(() => {
            location.reload()
          })
        })
      }
      if(res.msg.includes("token")){ //如果错误信息中包含“token”字样,则做如下处理
        store.dispatch('user/resetToken').then(() => {
          location.reload()   //页面刷新
        })
      }
      return Promise.reject(new Error(res.msg || 'Error'))
    } else {
      return res
    }
  },
  error => {
    console.log('err' + error) // for debug
    Message({
      message: error.message,
      type: 'error',
      duration: 5 * 1000
    })
    return Promise.reject(error)
  }
)

export default service

四、lang国际化实现

src/components/LangSelect/index.vue 修改国际化代码配置

<template>
  <el-dropdown trigger="click" class="international" @command="handleSetLanguage">  
    <div>
      <img class-name="international-icon" v-if="language==='en'" src="../../image/en.png" style="padding-top: 12px">
      <img class-name="international-icon" v-else src="../../image/cn.png" style="padding-top: 12px">
    </div>
    <el-dropdown-menu slot="dropdown">
      <el-dropdown-item :disabled="language==='zh-cn'" command="zh-cn">
        中文简体
      </el-dropdown-item>
      <el-dropdown-item :disabled="language==='zh-tw'" command="zh-tw">
        中文繁體
      </el-dropdown-item>
      <el-dropdown-item :disabled="language==='en'" command="en">
        English
      </el-dropdown-item>
    </el-dropdown-menu>
  </el-dropdown>
</template>

<script>
  import {user_lang} from "@/api/user";

  export default {
    computed: {
      language() {
        return this.$store.getters.language //从store中读出language的配置信息,getInfo会拿到lang信息
      },
      token() {
      return this.$store.getters.token  ////从store中读出language的配置信息 
    }
    },
    methods: {
      handleSetLanguage(lang) { //lang: zh-cn  zh-tw  en 三个参数
        if (this.token) {
          user_lang({  //调用语言切换接口
          lang: lang
          }).then(response => {
            this.$message.success(response.msg)
            this.$i18n.locale = lang
            this.$store.dispatch('user/setLanguage', lang)
            localStorage.setItem('language', lang)
            location.reload()
          })
        }else{
          this.$i18n.locale = lang
          this.$store.dispatch('user/setLanguage', lang)
          localStorage.setItem('language', lang)
          location.reload()
        }
      }
    }
  }
</script>

src/lang/index.js 国际化入口文件

五、views页面编码实现

前端页面编码在src/views/application路径下进行

六、配置文件实现

根目录下vue.config.js文件可进行IP配置,同时也是实现打开mock数据开关

IP配置

通过修改下面数据,可实现是调用mock数据还是IP服务数据

第三步、mock数据模拟

mock目录application-data.js文件产生模拟数据

const Mock = require('mockjs')

let application_list = []

const application_total = Mock.Random.integer(10, 20);
for (var i = 0; i < application_total; i++) {
    application_list.push({
        id: Mock.Random.increment(),
        name: Mock.Random.ctitle(2, 10),
        brief: Mock.Random.cparagraph(2, 50),
        type: Mock.Random.pick([0, 1]),
        status: Mock.Random.pick([3, 4]),
        publisherName: Mock.Random.cname(),
        publisherNo: Mock.Random.pick(['F', 'H']) + Mock.Random.integer(1000000, 9999999),
        publishAt: Mock.Random.date(),
        department: Mock.Random.pick(['HWTE', 'QT', 'Sub', 'SW', 'PD']),
        version: 'V' + Mock.Random.integer(0, 10),
        coverUrl: "@image('200x100', '@color', '#FFF', 'png', '!')",
        labels: 
           /*  {
                id: Mock.Random.increment(), */
                Mock.Random.ctitle(2, 10)
    })
}

module.exports = {
    application_list,
}

application.js文件实现数据与接口的绑定

const Mock = require('mockjs')
const application_data = require('./application-data.js')

//应用列表
const tool_list = application_data.application_list || []
//应用详情
const tool_detail = application_data.application_detail || []

module.exports = [
    //获取应用列表
    {
        url: '/aps/tool/query',  //绑定接口,页面访问该接口可直接获取mock数据
        type: 'post',
        response: config => { //分页处理
            let { page = 1, pageSize = 10 } = config.body
            page = page < 1 ? 1 : page
            pageSize = pageSize < 1 ? 1 : pageSize
            const mock_list = tool_list
            const page_list = mock_list.filter((item, index) => index < pageSize * page && index >= pageSize * (page - 1))
            return {
              code: 0,
              data: {
                pages: parseInt(pageSize),
                count: mock_list.length,
                page: page,
                pageSize: pageSize,
                list: page_list
              }
            }
          }
    },
    //获取应用详情
    {
        url: '/aps/tool/details',
        type: 'post',
        response: _ => {
            return {
                code: 0,
                msg: '',
                data: tool_detail  //tool_detail mock模拟出来的数据
            }
        }
    },
]

mock index.js文件配置

const Mock = require('mockjs')
const { param2Obj } = require('./utils')

const application = require('./application')

const mocks = [
  ...application,
]

function mockXHR() {
  Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send
  Mock.XHR.prototype.send = function() {
    if (this.custom.xhr) {
      this.custom.xhr.withCredentials = this.withCredentials || false

      if (this.responseType) {
        this.custom.xhr.responseType = this.responseType
      }
    }
    this.proxy_send(...arguments)
  }

  function XHR2ExpressReqWrap(respond) {
    return function(options) {
      let result = null
      if (respond instanceof Function) {
        const { body, type, url } = options
        result = respond({
          method: type,
          body: JSON.parse(body),
          query: param2Obj(url)
        })
      } else {
        result = respond
      }
      return Mock.mock(result)
    }
  }

  for (const i of mocks) {
    Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response))
  }
}

module.exports = {
  mocks,
  mockXHR
}

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值