前端框架

vue架构方案分析记录

2018-05-30  本文已影响301人  huangxiongbiao

一、采用基础技术

1、基础技术
vue,router(界面跳转),vuex(状态管理)
2、UI框架
element-ui
3、网络请求
axios

二、项目结构分层

image.png
build---打包配置的js
conf---配置打包的ip,port等基础参数
src--源代码
        api---网络请求数据层
        assets---图片静态资源
        components--基础组件
        router---路由层
        store---状态处理层
        styles---样式
        utils----通用工具
        pages(views)---视图层

三、各层次详解

1、网络层

简介:利用axios封装通用的请求类。方法中可以配置请求拦截和返回结果拦截,处理请求权限校验和相关报错的统一处理。另外根据界面分解成api层,利用工具类调用服务端接口返回数据。
目录参考:


image.png

部分参考样例代码:
工具类

import axios from 'axios'
import {Message} from 'element-ui'
import {MessageBox} from 'element-ui'
import store from '../store'
import {getToken, getID, getAccount, getAccountId} from '@/utils/auth'
axios.defaults.withCredentials=true;
// 创建axios实例
const service = axios.create({
  // baseURL: 'http://101.132.107.120:8080', // api的base_url
  timeout: 6000000
})

// request拦截器
service.interceptors.request.use(config => {
  if (store.getters.token) {
    config.headers['token'] = store.getters.token // 让每个请求携带自定义token 请根据实际情况自行修改
  }
  if (store.getters.JSESSIONID) {
    config.headers['JSESSIONID'] = store.getters.JSESSIONID
    // if (config.params) {
    //   config.url += '&' + Qs.stringify({'JSESSIONID': store.getters.JSESSIONID})
    // } else {
    //   config.url += '?' + Qs.stringify({'JSESSIONID': store.getters.JSESSIONID})
    // }
  }
  // if (store.getters.account) {
  //   config.headers['account'] = getAccount()
  // }
  if (store.getters.accountId) {
    config.headers['accountId'] = getAccountId()
  }
  config.headers['Test'] = 'zdzc'
  return config
}, error => {
  // Do something with request error
  console.log(error) // for debug
  Promise.reject(error)
})

// respone拦截器
service.interceptors.response.use(
  response => {
    /**
     * code为非20000是抛错 可结合自己业务进行修改
     */
    //  const res = response.data
    consoleLog('response', response.data)// for debug
    // 50008:非法的token; 50012:其他客户端登录了;  50014:Token 过期了;
    // if (response.data.statusCode === 4001) {
    //   MessageBox.confirm('你已被登出,可以取消继续留在该页面,或者重新登录', '确定登出', {
    //     confirmButtonText: '重新登录',
    //     cancelButtonText: '取消',
    //     type: 'warning'
    //   }).then(() => {
    //     store.dispatch('FedLogOut').then(() => {
    //       location.reload()// 为了重新实例化vue-router对象 避免bug
    //     })
    //   })
    // } else {
    return response.data
    // }
  },
  error => {
    console.log('err' + error)// for debug
    if (error.response.status === 401) {
      MessageBox.confirm('你已被登出,可以取消继续留在该页面,或者重新登录', '确定登出', {
        confirmButtonText: '重新登录',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        store.dispatch('FedLogOut').then(() => {
          location.reload()// 为了重新实例化vue-router对象 避免bug
        })
      })
    } else if (error.response.status === 404) {
      //TODO 处理404页面
    } else {
      if (error.response.data.statusCode === 40000) {
        MessageBox.confirm('你的访问授权已过期,请重新登录', '确定登出', {
          confirmButtonText: '重新登录',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
          store.dispatch('FedLogOut').then(() => {
            location.reload()// 为了重新实例化vue-router对象 避免bug
          })
        })
      } else {
        consoleLog('err', error.response.data)// for debug
        Message({
          message: error.response.data.message,
          type: 'error',
          duration: 5 * 1000
        })
      }
    }
    return Promise.reject(error)
  }
)

function consoleLog(tag, obj) {
  console.log(tag + ':' + JSON.stringify(obj))// for debug
}

export default service

api使用代码

import fetch from '@/utils/fetch'
import Qs from 'qs'

export function login (username, password) {
  return fetch({
    url: '/rest/token/login',
    method: 'post',
    data: Qs.stringify({ username: username, password: password })
  })
}

export function logout () {
  return fetch({
    url: '/rest/token/logout',
    method: 'post'
  })
}

actions: {
    // 登录
    Login({commit}, userInfo) {
      const username = userInfo.username.trim()
      return new Promise((resolve, reject) => {
        login(username, userInfo.password)
          .then(response => {
            const data = response.data
            setToken(data.token)
            setID(data.JSESSIONID)
            setAccount(data.account)
            setAccountId(data.accountId)
            commit('SET_TOKEN', data.token)
            commit('SET_JSESSIONID', data.JSESSIONID)
            commit('SET_ACCOUNT', data.account)
            commit('SET_ACCOUNT_ID', data.accountId)
            resolve()
          }).catch(error => {
            reject(error)
         })
      })
    },
2、路由层

简介:根据后台权限动态配置router。项目启动时,从后台获取相应的router数据,然后拼接组装新的router。利用生命钩子函数beforeEach、afterEach处理

目录参考


import工具方法,方便router引入component

module.exports = file => require('@/views/' + file + '.vue').default // vue-loader at least v13.0.0+

工具使用方法

const _import = require('./_import_component')

Vue.use(Router)

/**
 * hidden: true                   如果设置 `hidden:true` 将不在侧边栏上显示(默认是false)
 * alwaysShow: true               如果设置 true, 将一直显示, 不管子菜单的个数
 *                                如果未设置, 只有多于1个子菜单时才变为嵌套模式,否则不显示
 * redirect: noredirect           如果设置 `redirect:noredirect` 将不在面包屑导航中显示
 * name:'router-name'             该名称将用来设置 <keep-alive> (必需设置!!!)
 * meta : {
    roles: ['admin','editor']    根据角色控制页面权限 (可以设置多个角色)
    title: 'title'               该名称用来设置子菜单和面包屑导航
    icon: 'svg-name'             侧边栏上的小图标,
    noCache: true                页面是否缓存,默认为false,缓存
  }
 **/

// 不需要权限的路由表
export const constantRouterMap = [
  {path: '/login', component: _import('/login/index'), hidden: true},
]

nprogress 进度加载条
参考代码:
启动加载router数据代码

import router from './router'
import store from './store'
import NProgress from 'nprogress' // Progress 进度条
import 'nprogress/nprogress.css'// Progress 进度条样式
import {getToken} from '@/utils/auth' // 验权

const whiteList = ['/login']
router.beforeEach((to, from, next) => {
  NProgress.start()
  if (getToken() || store.getters.token) {
    if (to.path === '/login') {
      next({path: '/'})
    } else {
      if (store.getters.roles.length === 0) {
        store.dispatch('RoleAuth')
          .then(response => {
            const rowRouter = response.data.list
            store.dispatch('GenerateRoutes', {rowRouter}).then(() => {
              router.addRoutes(store.getters.addRouters)
              next({...to})
            })
          })
      } else {
        next()
      }
    }
  } else {
    if (whiteList.indexOf(to.path) !== -1) {
      next()
    } else {
      next('/login')
      NProgress.done()
    }
  }
})

router.afterEach(() => {
  NProgress.done() // 结束Progress
})

router组装代码

import { constantRouterMap } from '@/router/index'
const _import = require('@/router/_import_component')
const permission = {
  state: {
    routers: constantRouterMap,
    addRouters: []
  },
  mutations: {
    SET_ROUTERS: (state, routers) => {
      var last = [{ path: '*', redirect: '/404', hidden: true }]
      var addup = routers.concat(last)
      state.addRouters = addup
      state.routers = constantRouterMap.concat(addup)
    }
  },
  actions: {
    GenerateRoutes ({ commit }, data) {
      return new Promise(resolve => {
        var userRoutes = data.rowRouter
        var accessedRouters = []

        userRoutes.forEach((item) => {
          // 遍历第一层路由
          let userRoutesItem = {}
          var path = item.url
          userRoutesItem.path = item.url
          userRoutesItem.name = item.name
          userRoutesItem.icon = item.meta.icon
          userRoutesItem.noDropdown = item.noDropdown
          userRoutesItem.component = (resolve) => require(['@/views/layout/Layout'], resolve)

          // 第一层路由是否有子路由
          if (item.children) {
            let childrenRoute = []
            var firstChild = item.children

            userRoutesItem.redirect = path + '/' + firstChild[0].url

            // 遍历第一层路由的子路由
            firstChild.forEach((childone) => {
              let childrenRouteItem = {}
              childrenRouteItem.path = childone.url
              childrenRouteItem.name = childone.name
              if(childone.meta){
                childrenRouteItem.btnauth = childone.meta.edit
              }
              var url = childone.url
              childrenRouteItem.component = _import(path + '/' + url)

              // 第一层路由子路由是否有子路由
              if (childone.children) {
                let childtwoRoute = []
                var secondChild = childone.children

                // 遍历第一层路由子路由的子路由
                secondChild.forEach((childtwo) => {
                  let childtwoRouteItem = {}
                  childtwoRouteItem.path = childtwo.url
                  childtwoRouteItem.name = childtwo.name
                  childtwoRouteItem.hidden = childtwo.hidden
                  var lastPath = childtwo.url
                  childtwoRouteItem.component = _import(path + '/' + lastPath)
                  childrenRoute.push(childtwoRouteItem)
                })
              }
              childrenRoute.push(childrenRouteItem)
            })
            userRoutesItem.children = childrenRoute
          } else {
            let childrenRoute = []
            let childrenRouteItem = {}
            userRoutesItem.redirect = item.url + '/index'
            childrenRouteItem.path = 'index'
            childrenRouteItem.component = _import(path + '/index')
            childrenRoute.push(childrenRouteItem)
            userRoutesItem.children = childrenRoute
          }
          accessedRouters.push(userRoutesItem)
        })
        commit('SET_ROUTERS', accessedRouters)
        resolve()
      })
    }
  }
}

export default permission

3、状态处理层

简介:中心index引入各个文件的store处理,分开处理
目录:


image.png

参考代码:
index

import Vue from 'vue'
import Vuex from 'vuex'
import app from './modules/app'
import user from './modules/user'
import permission from './modules/permission'
import getters from './getters'
import 'babel-polyfill'

Vue.use(Vuex)

const store = new Vuex.Store({
  modules: {
    app,
    user,
    permission
  },
  getters
})

export default store

单个实例

import {login, logout} from '@/api/login'
import {getRoleAuth, getUserRole} from '@/api/role'
import {
  getToken,
  setToken,
  removeToken,
  getID,
  setID,
  removeID,
  getAccount,
  setAccount,
  removeAccount,
  getAccountId,
  setAccountId
} from '@/utils/auth'

const user = {
  state: {
    token: getToken(),
    JSESSIONID: getID(),
    account: getAccount(),
    accountId:getAccountId(),
    name: '',
    avatar: '',
    roles: []
  },

  mutations: {
    SET_TOKEN: (state, token) => {
      state.token = token
    },
    SET_JSESSIONID: (state, JSESSIONID) => {
      state.JSESSIONID = JSESSIONID
    },
    SET_ACCOUNT: (state, account) => {
      state.account = account
    },
    SET_ACCOUNT_ID: (state, accountId) => {
      state.accountId = accountId
    },
    SET_NAME: (state, name) => {
      state.name = name
    },
    SET_AVATAR: (state, avatar) => {
      state.avatar = avatar
    },
    SET_ROLES: (state, roles) => {
      state.roles = roles
    }
  },

  actions: {
    // 登录
    Login({commit}, userInfo) {
      const username = userInfo.username.trim()
      return new Promise((resolve, reject) => {
        login(username, userInfo.password)
          .then(response => {
            const data = response.data
            setToken(data.token)
            setID(data.JSESSIONID)
            setAccount(data.account)
            setAccountId(data.accountId)
            commit('SET_TOKEN', data.token)
            commit('SET_JSESSIONID', data.JSESSIONID)
            commit('SET_ACCOUNT', data.account)
            commit('SET_ACCOUNT_ID', data.accountId)
            resolve()
          }).catch(error => {
            reject(error)
         })
      })
    },

    // 获取/更新用户信息
    RoleAuth({commit}) {
      return new Promise((resolve, reject) => {
        getUserRole()
          .then(response => {
            let userRole = response.data
            commit('SET_ROLES', userRole.role)
            commit('SET_NAME', userRole.name)
            commit('SET_AVATAR', userRole.avatar)
            return resolve(getRoleAuth());
          })
      })
    },

    // 登出
    LogOut({commit, state}) {
      return new Promise((resolve, reject) => {
        logout().then(() => {
          commit('SET_TOKEN', '')
          // commit('SET_ROLES', [])
          removeToken()
          removeID()
          removeAccount()
          resolve()
        }).catch(error => {
          reject(error)
        })
      })
    },

    // 前端 登出
    FedLogOut({commit}) {
      return new Promise(resolve => {
        commit('SET_TOKEN', '')
        removeToken()
        resolve()
      })
    },
  }

}

export default user

4、工具包

简介:一般存放请求类,校验的正则工具,时间处理等等工具方法

5、打包配置

分为开发配置和上线配置,参考
打包js

'use strict'
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const merge = require('webpack-merge')
const path = require('path')
const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
const portfinder = require('portfinder')

const HOST = process.env.HOST
const PORT = process.env.PORT && Number(process.env.PORT)

function resolve (dir) {
  return path.join(__dirname, '..', dir)
}

const devWebpackConfig = merge(baseWebpackConfig, {
  module: {
    rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
  },
  // cheap-module-eval-source-map is faster for development
  devtool: config.dev.devtool,

  // these devServer options should be customized in /config/index.js
  devServer: {
    clientLogLevel: 'warning',
    historyApiFallback: {
      rewrites: [
        { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
      ],
    },
    hot: true,
    contentBase: false, // since we use CopyWebpackPlugin.
    compress: true,
    host: HOST || config.dev.host,
    port: PORT || config.dev.port,
    open: config.dev.autoOpenBrowser,
    overlay: config.dev.errorOverlay
      ? { warnings: false, errors: true }
      : false,
    publicPath: config.dev.assetsPublicPath,
    proxy: config.dev.proxyTable,
    quiet: true, // necessary for FriendlyErrorsPlugin
    watchOptions: {
      poll: config.dev.poll,
    }
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.env': require('../config/dev.env')
    }),
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
    new webpack.NoEmitOnErrorsPlugin(),
    // https://github.com/ampedandwired/html-webpack-plugin
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: 'index.html',
      inject: true,
      favicon: resolve('favicon.ico')
    }),
    // copy custom static assets
    new CopyWebpackPlugin([
      {
        from: path.resolve(__dirname, '../static'),
        to: config.dev.assetsSubDirectory,
        ignore: ['.*']
      }
    ])
  ]
})

module.exports = new Promise((resolve, reject) => {
  portfinder.basePort = process.env.PORT || config.dev.port
  portfinder.getPort((err, port) => {
    if (err) {
      reject(err)
    } else {
      // publish the new Port, necessary for e2e tests
      process.env.PORT = port
      // add port to devServer config
      devWebpackConfig.devServer.port = port

      // Add FriendlyErrorsPlugin
      devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
        compilationSuccessInfo: {
          messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],
        },
        onErrors: config.dev.notifyOnErrors
        ? utils.createNotifierCallback()
        : undefined
      }))

      resolve(devWebpackConfig)
    }
  })
})

打包配置

'use strict'
// Template version: 1.3.1
// see http://vuejs-templates.github.io/webpack for documentation.

const path = require('path')

module.exports = {
  dev: {

    // Paths
    assetsSubDirectory: 'static',
    assetsPublicPath: '/',
    proxyTable: {
      // 真实数据接口代理
      '/rest': {
        // target: 'http://192.168.1.53:8080',
        target: 'http://192.168.3.47:8080',
        changeOrigin: true,
        pathRewrite: {
          '^/rest': ''
        }
      }

      // Mock数据接口代理:Easy Mock
      // '/rest': {
      //   target: 'https://easy-mock.com/mock/5ac1dd803d15d57d988308ab/dam/rest',
      //   changeOrigin: true,
      //   pathRewrite: {
      //     '^/rest': ''
      //   }
      // }
    },

    // Various Dev Server settings
    // host: '192.168.1.53', // can be overwritten by process.env.HOST
    host: '192.168.3.47',
    port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
    autoOpenBrowser: true,
    errorOverlay: true,
    notifyOnErrors: true,
    poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-

    // Use Eslint Loader?
    // If true, your code will be linted during bundling and
    // linting errors and warnings will be shown in the console.
    useEslint: false,
    // If true, eslint errors and warnings will also be shown in the error overlay
    // in the browser.
    showEslintErrorsInOverlay: false,

    /**
     * Source Maps
     */

    // https://webpack.js.org/configuration/devtool/#development
    devtool: 'cheap-module-eval-source-map',

    // If you have problems debugging vue-files in devtools,
    // set this to false - it *may* help
    // https://vue-loader.vuejs.org/en/options.html#cachebusting
    cacheBusting: true,

    cssSourceMap: true
  },

  build: {
    // Template for index.html
    index: path.resolve(__dirname, '../dist/index.html'),

    // Paths
    assetsRoot: path.resolve(__dirname, '../dist'),
    assetsSubDirectory: 'static',
    assetsPublicPath: '/',

    /**
     * Source Maps
     */

    productionSourceMap: true,
    // https://webpack.js.org/configuration/devtool/#production
    devtool: '#source-map',

    // Gzip off by default as many popular static hosts such as
    // Surge or Netlify already gzip all static assets for you.
    // Before setting to `true`, make sure to:
    // npm install --save-dev compression-webpack-plugin
    productionGzip: false,
    productionGzipExtensions: ['js', 'css'],

    // Run the build command with an extra argument to
    // View the bundle analyzer report after build finishes:
    // `npm run build --report`
    // Set to `true` or `false` to always turn it on or off
    bundleAnalyzerReport: process.env.npm_config_report
  }
}

上一篇下一篇

猜你喜欢

热点阅读