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.json
API描述文件,描述了接口的请求、返回格式、处理操作等 -
setting.json
应用配置
应用初始化
应用初始化时调用此方法,包括初始化应用的全局变量、数据系统、权限系统、Vue扩展挂载等,不同应用可以根据业务的不同自行增减初始化应用代码
export const initApp = () => {
initVue() // 常用方法挂载到Vue原型链上,在组件内部就可以进行this.bindName的调用
initToken() // 初始化应用的Token
initRouter() // 初始化路由
initFrame() // 初始化其他应用嵌入本应用策略
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
}
页面模块介绍
页面包括标题组件、左侧菜单组件、返回顶部组件、换主题组件、标签页切换组件和业务组件
权限系统
多角色下,多层级、渐进、严谨的权限系统
中大型企业中控业务模块众多,角色也非常丰富,因此权限系统的控制就变得非常的重要,它是用来描述不同角色的人所具有的权限
往前后端笼统的讲,权限系统分为页面权限和数据接口权限,而中控台隶属页面权限,由于是单页应用,页面权限的判断全部在浏览器内部判断,因此提取之后把权限分为三级,即登陆前白名单、登陆后白名单及登陆后的权限页面
当非登陆前访问登陆之后的页面时一律跳转到登陆,使其登陆,登陆后自动显示登陆白名单页面,并且定位到权限页面,用户无权时跳转到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>
未完待续...