中大型平台Vue单页应用的设计思路、使用指南

PS:文章很长,读完将花费你20分钟

平台级应用的基础架构背景介绍

平台级的应用具有很强的目的性,一个应用干一件事情,会配合平台已有的很多已有的中间件如用户系统、权限系统等软件,包括这几年微服务的兴起,平台级别的应用颗粒级别越来越低,目的性越来越强,先简单的介绍一下平台级的应用基础架构大概是怎样的(具体到实际会更加复杂、多变)。

中大型企业架构

具有类似的微服务接口

统一的码表管理,统一的模块调用如计费模块、支付通用模块、数据分发、公告发布板块等

统一的身份认证平台

由于采用统一的身份认证,和权限平台,平台级别应用可以相互跳转、应用和应用直接可以相互嵌套,甚至可以做到多个应用一个入口(参考阿里云中控架构)

其他企业接入

支持其他企业的应用接入平台,共享平台数据,实现共通的平台数据交换

扯了这么多,具体到平台应用内部,尤其是本章的主题(前端基于Vue的应用开发),具体到代码是一个什么样的软件架构呐,本文将逐步介绍现有的前端框架,从设计思路到使用指南再到最后的常见问题。

企业级中控设计思路

中间件介绍

从头开始造轮子的时代已过去,中间件是现代开发中起到了很大的作用,下面介绍一下具体用到的一些中间件

  • Webpack进行打包
  • 使用了Vue的脚手架,用到了Vue全家桶:Vue + Vue-Router + Vuex
  • 公司的组件库,适合公司特定的业务需求,借鉴了Element组件库的优秀思想
  • Echarts做图表统计
  • ES6新增API兼容代码(主要是Promise和Fetch的API兼容)
目录结构介绍

遵循一定的目录规范,高度模块化的应用开发体验

目录结构
  • assets图片,样式资源
  • common全局变量、通用模块继承类
  • ep-ui组件库
  • framework框架页面存放地址,包括应用中控(菜单、标题、标签切换)、错误页面、登陆页面
  • lang多语言文件
  • lib直接引入依赖(如qrcode.js等)
  • router路由列表
  • store全局状态基
  • template业务层封装组件
  • utils工具方法
  • views业务视图
  • api.jsonAPI描述文件,描述了接口的请求、返回格式、处理操作等
  • setting.json应用配置
应用初始化

应用初始化时调用此方法,包括初始化应用的全局变量、数据系统、权限系统、Vue扩展挂载等,不同应用可以根据业务的不同自行增减初始化应用代码

export const initApp = () => {
  initVue()    // 常用方法挂载到Vue原型链上,在组件内部就可以进行this.bindName的调用
  initToken()  // 初始化应用的Token
  initRouter() // 初始化路由
  initFrame()  // 初始化其他应用嵌入本应用策略

  new Vue({
    router,
    store,
    render: h => h(App)
  }).$mount('#app')
}
页面模块介绍

页面包括标题组件、左侧菜单组件、返回顶部组件、换主题组件、标签页切换组件和业务组件

Chrome插件截图
页面详细
权限系统

多角色下,多层级、渐进、严谨的权限系统

中大型企业中控业务模块众多,角色也非常丰富,因此权限系统的控制就变得非常的重要,它是用来描述不同角色的人所具有的权限

往前后端笼统的讲,权限系统分为页面权限和数据接口权限,而中控台隶属页面权限,由于是单页应用,页面权限的判断全部在浏览器内部判断,因此提取之后把权限分为三级,即登陆前白名单、登陆后白名单及登陆后的权限页面

当非登陆前访问登陆之后的页面时一律跳转到登陆,使其登陆,登陆后自动显示登陆白名单页面,并且定位到权限页面,用户无权时跳转到401页面,当无页面时跳转404页面,这就是一整个权限系统的运行逻辑

页面权限是通过懒加载的形式获取的,即跳转到权限页面时再通过接口获取,并缓存在内部变量中、并且在每次初始化的时候进行调用

接口中指定返回Code代表已经过期,自动清空过期凭证,跳转到登陆页面

这种权限系统的设计造就了单页应用相对安全的页面及数据访问

数据请求统一配置

后端接口的定义无论多少千奇百怪,但就是这种多样化的接口,都可通过配置api.json来主观描述接口格式,做到最终暴露给业务层开放的API输入输出保持一致

业务层页面动态keep-alive

缓存点击过的页面

原先的模式为一个应用开很多标签页,导致了嵌套很多的iframe,现如今单页应用讲究一个快字,因此原先的iframe模式代替为vue-router的跳转,并且缓存的内容需要做到动态的变化,即关闭标签页时清空页面缓存

全局状态基

通过全局状态基,可以做到很多事情,如全应用通用的缓存模块、应用Jsp的模版数据获取等

从原来是Jsp的应用架构向动静分离的应用架构迁移很容易丢失的一点是模板,有可能全局用到的一些模板参数在静态页面是一件很难做到的事情

把这些模板参数存入全局的状态基,并且在应用初始化的时候获取数据,缓存在Vuex内部,这样造就了全局共享参数,也减轻了服务器端的消耗

页面通用模块继承

管理中控大部分的页面都是数据表格搜索、表单录入这些功能,因此把这些公共的部分单独剥离,整合

每个组件继承通用模块,通过整合了管理中控最常用的数据表格模块的内容,大大加快了模块的开发,一些通用模块只需要进行数据结构的定义和html片段的编写即可做到模块的开发,开发起来及其迅捷

企业级中控使用指南

框架旨在满足支撑各种类型应用的开发,因此可以对模块进行增减,本章将详细介绍如何通过使用和改造框架,做到多级多层应用可用

开发、生产、测试分开打包

不同环境的应用参数会发生变化,因此做了三种不同策略辅助

运用webpack插件ProvidePlugin做到不同环境不同配置,解决了质量监控室打包完后部署人员还需解压修改静态文件的问题

// 打包命令
"build:dev": "npm run entry:route && cross-env BUILD_ENV=development node buildScript/build.js",
"build:prod": "npm run entry:route && cross-env BUILD_ENV=production node buildScript/build.js",
"build": "npm run entry:route && cross-env BUILD_ENV=test node buildScript/build.js"
// webpack配置
new webpack.ProvidePlugin({
  ENV: "../../config/common-"+ (process.env.BUILD_ENV || "development")
})
// 配置文件config/common-环境.js
module.exports = {
  baseUrl: 'http://api.com',
  otherServer: 'http://api.other.com'
}
// 内部调用
global.HOST = ENV.baseUrl
global.OTHER_SERVER= ENV.otherServer
router.js自动生成

自动生成路由描述,提供自动化思路

在应用启动和打包之前自动生成路由,达到配置驱动的目的,如应用需要修改生成模板,手动修改buildScript/entry/route-entry.js

{
  "biz": {  // 业务层页面
    "/test": {
      "icon": "home",  // 图标
      "name": "测试页面",  // 名字
      "router": "test/testRouter"  // 页面路径,此路径配置为view/biz/test下的testRouter.vue文件
    }
  },
  "sys": {  // 系统层页面(首页、管理员)
    "/home": {
      "icon": "home",
      "name": "首页",
      "router": "home"  // 页面路径,此路径配置为view/sys/test下的testRouter.vue文件
    },
  }
}
权限系统和菜单的增量开发

不同的应用架构具有完全不同的可定制化内容,这时候通过一定的方法赋写,不修改框架组件(这点非常重要,因为有可能框架面临了升级,自定义组件会对升级和版本控制造成一定的困扰)即可做到不同应用之间的可定制化

  • 对于中大型项目
    中大型项目具有高度权限、菜单等可配置的特点,即用户的权限、角色、菜单和用户信息都通过后端接口进行获取,这时候无需修改框架,直接修改数据库、在平台配置权限、应用参数即可,然后通过每次应用初始化的时候会通过接口获取到应用各项内容

  • 对于小型应用
    小型应用的页面有可能是很单一,只要区区几个菜单项,此时用后端接口获取就变得很鸡肋,因此此类应用可以通过赋写权限系统实现方法做到

1、对于多角色、页面权限交给前端维护的应用而言

多角色不改变原有模式,应用接口请求到用户角色之后,单独开设一个文件专门用来描述此角色对于的页面权限,直接在以下代码块下方进行判断赋值即可

// 文件:utils/oauth.js
function getUserInfo (fn) {
  post('getUserInfo', undefined).then (json => {
    // 后台过来的menu
    let { data } = json
    // 此处改造为通过传输过来的角色信息获取对应的页面权限
    let router = menuJson[data.userRole]
    dispatchStore(router, data.userInfo)
    fn(router)
  }).catch (e => {
    Message({ type: 'danger', message: '用户信息获取失败,请稍后刷新再试!' })
  })
}
2、对于单角色而言

单角色就非常的便捷,页面只有两种权限,登陆前白名单和登陆后,因此直接修改router的beforeEach即可

const whiteList = settings.whiteList           // 登陆前不重定向白名单
const loginWhiteList = settings.loginWhiteList // 登录后不重定向白名单(此模式下这个无效)
if (getToken() !== '') {
  if (to.path === '/login') {
    next('/home')
  } else {
    next()
} else {
  whiteList.indexOf(to.path) !== -1
    ? next()
    : next('/login')
}
前后端数据交互设计

Fetch的基础上封装了一层内容,添加了接口调用超时处理、response status校验、数据校验、验签等

方法分为了三种,post、get和request,其中前两种都调用了request,并且对返回进行框架上的统一处理,因此有新接口接入应用时只需在request的基础上添加自定义请求方法进行增量更新

此方法的调用保证了数据输入的一致性,下面简单的介绍一下如何进行配置和数据调用。

// 调用
this.$post('getUserInfo', { requestKey: "requestValue" }).then (json => {
    // 正确返回
  }).catch (e => {
    // 错误返回
  })
this.$get('getTest', { requestKey: "requestValue" }).then (json => {
    // 正确返回
  }).catch (e => {
    // 错误返回
  })

// api.json配置
{
  "post": {
    "dataType": "form",
    "url": "/api/userInfo"
  },
  "get": {
    "getTest": "/api/getTest"    // 如只有一个参数url,可省略为如此
  }
}

// 配置参数介绍和描述
{
  "url": "urlString"      // 请求API,可以为绝对路径,不为绝对路径自动拼接baseurl
  "oauth": true,          // 是否带token(默认为true)
  "dataType": "json",     // 数据请求格式(form、json、file、html,默认为json)
  "rtnType": "json"       // 返回格式(目前暂时只支持json)
  "showSuccess": false,   // 成功是否显示弹框(默认为false)
  "showError": true,      // 失败是否显示弹框(默认为true)
}

可以很明显的发现API非常的简洁,让开发人员完全专注于业务开发而无需关心返回值错误校验等反复的操作,回调进入方法直接处理

页面模板开发

为了做到快速开发的目的,需要遵循一定的开发模板,下面就通过代码具体介绍一下

<template>
  <div class="panel-main-content">
    <!--筛选栏组-->
    <div class="search-card contents-card card-margin">
      <div class="panel panel-default">
        <div class="card-title zero-padding"><span class="weight">菜单列表</span></div>
        <ep-form ref="searchForm" :form="searchForm" name-width="90px">
          <ep-row :gutter="7">
            <ep-col :col="6">
              <ep-form-item attr="eq_menuCode" label="菜单编码">
                <ep-input placeholder="菜单编码" v-model="searchForm.eq_menuCode" name="eq_menuCode" :maxlength="20"></ep-input>
              </ep-form-item>
            </ep-col>
            <ep-col :col="6">
              <ep-button type="warning" size="small" @click="reset('searchForm')">重置</ep-button>
              <ep-button type="primary" size="small" @click="refresh(true)" icon="search" :loading="loading">查询</ep-button>
            </ep-col>
          </ep-row>
        </ep-form>
      </div>
    </div>
    <!--表格-->
    <div class="ep-card card-margin relative">
      <div v-if="selectLength !== 0" class="ep-table-selected-header">
        选择了 {{ selectLength }} 项
        <span style="text-align: right">
          <ep-button type="text" icon="trash-a" @click="doDelete"></ep-button>
        </span>
      </div>
      <div class="card-body">
        <div class="block">
          <ep-button type="primary" size="small" @click="doAdd" icon="plus">新增</ep-button>
          <ep-button type="success" size="small" @click="doSave" icon="edit">保存</ep-button>
          <ep-button type="warning" size="small" @click="doReset" icon="pricetag">重置</ep-button>
          <ep-button type="primary" size="small" @click="doRefresh" icon="ios-refresh">刷新</ep-button>
        </div>
        <div class="block">
          <ep-table ref="table" :data="ep_data" :height="700"
            @selection-change="handleSelectionChange" can-edit :loading="loading">
            <!-- 表格item在此 -->
          </ep-table>
        </div>
        <div class="block">
          <ep-pager right @size-change="handleSizeChange" @change="handleCurrentChange"
            :now-page="ep_page.offset" :page-size="ep_page.limit" :total-num="totalcount"></ep-pager>
        </div>
      </div>
    </div>
  </div> 
</template>

<script>
  import misList from 'src/common/mislist'

  export default {
    name: 'menu',  // 保持和文件名一致,否则keep-alive不会动态缓存

    extends: misList,  // 务必继承

    created () {
      this.refresh(true)  // 调用继承方法
    },

    mounted () {
      
    },

    methods: {
      searchCallback (json) {
        // 搜索成功回调,做特殊处理在此
      }
    }

    data () {
      return {
        loading: false,
        listApi: 'menusSearch', // 搜索,取api.json里面的key值
        saveApi: 'menuSave',    // 保存,取api.json里面的key值
        settings: {
          pk: 'id'    // 主键
        },
        searchForm: {    // 筛选条件
          eq_menuCode: ''
        },
        selectLength: 0,
        totalcount: 0,
        ep_page: {    // 分页
          limit: 10,
          offset: 1
        },
        ep_data: []    //表格数据
      }
    }
  }
</script>

未完待续...

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,796评论 25 707
  • 您是否有为父母选择保险的需求 父母的年纪大了,身体的很多机能都开始衰退,各种疾病发生的几率都在增高,保险可选择种类...
    妈咪保贝阅读 248评论 0 0
  • 我们班一女生,可能有点呆,社会阅历少,特别容易相信别人,加之班上阳盛阴衰,我们班男生都对她宠爱有加。可就是这个我们...
    黄诩阅读 298评论 1 3
  • 《小刀,吃包子图》 作者:王薇 王薇,画动画的,据说职位挺高,我们都称她“王总” 曾经,大力支持我的消防采访,并用...
    小米rt8阅读 315评论 4 4
  • 今天的长沙下了一点小雨,碰巧一变天就感冒的我,这时候一个人捧着手机窝在出租房里无所世事。 突然很怀恋广州的天气,这...
    微艳阅读 188评论 0 3