前言
本文是跟随抖音
哲玄前端学习的《大前端全栈实践》课程之后的总结和感悟。
背景
业务开发过程中(尤其是后台项目),会有很多表单、表格这种相似的、重复开发的页面,针对这部分可以利用DSL进行描述、在运行时进行动态渲染,从而降低开发时间。因此elpis诞生了,框架核心采用:Vue3+Koa+Webpack5+Element Plus 进行构建
框架设计
框架使用DSL对页面进行描述,前端获取到DSL配置进行动态化地组件渲染操作
核心设计思想
DSL和领域驱动设计、AOP理念、约定大于配置、SSOT单一事实来源、微内核架构
DSL和领域驱动设计
- DSL领域特定语言
"没有什么是一层中间层不能解决的,如果有,那就再加一层",DSL正是这一层中间层的产物。
DSL是应用于某一特定领域的自定义语言,比如sql就是,用于对具体的各个命令式的操作进行上一层的封装,以提升可读性、跨平台兼容等,通常DSL伴随着解析引擎和执行引擎,解析引擎通过经典的编译原理步骤(词法分析、语法分析、中间代码生成)去生成具体的可被执行引擎执行和优化的描述性数据结构(比如:AST),执行引擎拿到它进行执行。
elpis需要DSL用来描述页面,由于js天然支持json到js对象的转换,所以采用基于json的扩展格式作为DSL,对于执行引擎部分,elpis需要做的是对DSL进行组件映射和属性设置等,核心是Vue3的动态组件绑定(<component :is />)。
- 领域驱动设计
通过通用语言,实现业务与技术的连接,通过限界上下文将连接的一致性限制在有效的业务边界内
简单理解就是:通过描述业务的方式,去驱动整个项目的运行,elpis的核心正是这一点,通过对DSL的描述,去驱动视图的渲染和交互。
AOP面向切面编程
AOP是一种代码组织理念,核心是对多处使用的、来源广泛的逻辑进行抽取和封装,优点是后续:修改只改一处、扩展只加一步。常见的AOP理念有:装饰器、服务中间件、Proxy代理
elpis的AOP理念首先体现在中间件:BFF层使用Koa搭建服务,通过Koa洋葱圈模型的中间件,elpis能够对所有的请求进行统一的处理和逻辑判断。
约定大于配置
约定大于配置是一种通过约束行为来替代配置的思想,它将行为动作赋予语义化,以此来描述配置,常见的实现如:next.js中通过指定在router下创建文件来直接创建路由,从而避免手动注册一遍。
elpis中核心采用这一思想,通过开发者在项目中创建config、middleware、extend、controller、router、service等文件,来自动注册上对应的功能,从而避免还需要再手动注册一遍,大大减轻了开发者的负担。
单一事实来源
单一事实来源是一种数据管理原则,在软件系统中如果有多处维护着相同的约定,就会导致数据冗余和状态同步成本,系统的可维护性就会降低。单一事实来源就是让我们去避免这样的操作。
在用DSL去描述页面时,通常多个系统之间也有共用的相似部分,如果分别配置会让后续的新增和修改都比较麻烦,elpis采用数据模型继承机制,抽象出模型的概念,让同一类型的系统能够继承自一个数据来源,从而大大降低了系统配置的复杂度,提高了可维护性。
微内核架构
微内核架构是一种可扩展性极强的架构风格,它将自身各个核心流程通过hook的方式暴露出去,从而让外部扩展可以接管自身的全流程。常见的库如:webpack、eslint等
elpis的BFF层(elpis-core)采用微内核架构,将app运行中的各个步骤:路由、中间件、继承扩展、控制器等,暴露给开发者,让其能够通过自定义loader来控制整个项目的运行。
实现步骤
BFF运行时:elpis-core
elpis-core采用Koa搭建服务,采用微内核架构扩展koa中间件、前后端路由、后端接口schema和controller等
elpis-core会通过Koa创建一个app实例,然后通过如下loader将其挂载到app上并进行使用:
- router-schema:制定 API 接口的 Schema 规范,实现接口配置标准的统一化。
- router:管理页面请求的路由映射,控制请求的具体流向与分发路径。
- controller:承担业务逻辑处理职责,针对具体业务场景进行响应处理。
- service:构成服务处理层,对通用服务逻辑进行封装,向下层业务提供能力支持。
- config:用于存放项目的基础配置数据,涵盖版本号、项目名称等核心配置信息。
- extend:提供项目功能的扩展能力,支持接入日志记录、数据库连接等额外功能模块。
router-schema
router-schema通过ajv来对不符合要求的请求进行拦截、从而保证数据库的安全、避免非常规请求对导致系统数据异常
router
router通过路由分发,将请求分流成视图请求,和数据请求,视图请求通过nunjunks进行模板渲染,数据请求通过操作数据库等获取数据
controller
controller用于处理业务逻辑,暴露在router中作为api进行使用
service
service用于操作数据库去获取数据,暴露在controller中作为api进行使用
config
config用于对项目进行通用配置,作为属性挂载到app上
extend
extend为应用扩展额外的模块和功能,以便在全局进行获取与使用
前端工程化
elpis采用Webpack5来完成前端工程化,核心是从entry入口处,将项目资源进行编译打包、模块拆分、压缩优化等,以便Koa能够通过这些产物给浏览器请求提供资源
开发环境建设
elpis的开发服务器使用Express进行实现,由于我们的模板渲染需要Koa的能力,因为不能直接使用webpack-dev-server,而是去自建开发服务器,具体流程如下:
- webpack开发服务器,会监听文件变化、重新编译。(
webpack-dev-middleware中间件) - 修改代码之后,webpack重新编译
- webpack.dev.config中配置了HMR插件:
webpack-hot-module-replacement-plugin,打包构建之后,生成更新文件:.hot-update.json 和 .hot-update.js - webpack.dev.config中的entry配置了
webpack-hot-middleware/client,以便将热更新脚本打包进去 - 开发服务器监听到
.hot-update.json,通过SSE或将其推送到浏览器。(webpack-hot-middleware中间件) - 浏览器接收到消息,请求
对应的.hot-update.js进行执行,执行模块替换
领域模型架构
elpis使用DSL去描述页面视图,使用继承扩展的方式抽象公共部分,通过DSL视图描述加上动态组件的方式去驱动视图渲染。
具体的DSL配置如下:
export default {
mode: 'dashboard', // 模板类型, 不同模板类型对应不一样的模板数据结构
name: '', // 名称;
desc: '', // 描述
icon: '', // 图标
homePage: '', // 首页(项目配置)
// 头部菜单
menu: [{
key: '', // 菜单唯一描述
name: '', // 菜单名称
menuType: '', // 枚举值 group 为有下拉子菜单 / module 为无子菜单 ,link表示无路由的链接
// 当menuType为group时,可填
subMenu: [{
// 可递归 menuItem
// 更多菜单项
}],
// 当menuType为module时,可填
moduleType: '', // 枚举值 sider 为侧边栏 /iframe 为第三方页面 / custom 为自定义页面 / schema 为配置式页面
// 当moduleType为sider时,可填
siderConfig: {
menu: [{
// 可递归 menuItem(除moduleType === sider 时)
}] // ... 更多菜单项
},
// 当moduleType为iframe时,可填
iframeConfig: {
path: '' // iframe 路径
},
// 当moduleType为custom时,可填
customConfig: {
path: '' // 自定义组件路径
},
// 当moduleType为schema时,可填
schemaConfig: {
api: '', // 数据源API (遵循 RESTFUL 规范)
// 板块数据结构
schema: {
type: 'object',
properties: { // 板块属性
key: {
// 标准 schema 配置(占位)
type: '', // 字段类型
label: '', // 字段名称
// 字段在 table 中的相关配置
tableOption: {
// 标准 el-table-column 配置(占位)
toFixes: 2, // 数字类字段保留小数位数
visible: true // 是否在 表单 中显示
},
searchOption: {
// 标准 el-component-column 配置(占位)
comType: '', // 配置组件类型 input/select....
default: '', // 默认值
// 当 comType 为 select时
enumList: [], // 下拉框可选值
// 当 comType 为 dynamicSelect时
api: '',
},
// 字段在不同动态 component 中的相关配置, 前缀对应 componentConfig 中的键值
// 比如 componentConfig.createForm 这里就对应 createFormOption 的配置
// 字段在 createForm 中相关配置
createFormOption: {
// 标准 el-component-column 配置(占位)
comType: '', // 控件类型 input/select....
visible: true, // 是否在 表单 中显示 默认true
disabled: false, // 是否禁用
default: '', // 默认值
// 当 comType 为 select时
enumList: [], // 下拉框可选值
},
// 字段在 editForm 中相关配置
editFormOption: {
// 标准 el-component-column 配置(占位)
comType: '', // 控件类型 input/select....
visible: true, // 是否在 表单 中显示 默认true
disabled: false, // 是否禁用
default: '', // 默认值
},
// 字段在 detailPanel 中相关配置
detailPanelOption: {
// 标准 el-component-column 配置(占位)
comType: '', // 控件类型 input/select....
visible: true, // 是否在 表单 中显示 默认true
disabled: false, // 是否禁用
default: '', // 默认值
},
},
// ... 用户可扩展
},
required: [],
},
// 表单配置
tableConfig: {
headerButtons: [{ // 头部按钮组
label: '', // 按钮名称
eventKey: '', // 按钮事件名
eventOption: {
// 当eventKey === 'showComponent' 时,可填
conName: '', // 组件名称
}, // 按钮配置
// 标准 el-button 配置(占位)
}], // ... 更多按钮项
rowButtons: [{ // 行为按钮组
label: '',
eventKey: '',
eventOption: {
// 当eventKey === 'showComponent' 时,可填
conName: '', // 组件名称
// 当eventKey === 'remove' 时,可填
params: {
// paramKey = 参数键名
// rowValueKey = 参数值 当格式为 schema::tableKey 的时候,到 table中找到响应的字段
paramKey: 'rowValueKey'
}
},
// 标准 el-button 配置(占位)
}]
}, // table 配置
searchConfig: {}, // search-bar 配置
// 动态组件 相关配置
componentConfig: {
// create-form 表单相关配置
createForm: {
title: '', // 创建表单标题
saveBtnText: '', // 保存按钮名称
},
// edit-form 表单相关配置
editForm: {
mainKey: '', // 主键字段
title: '', // 编辑表单标题
saveBtnText: '', // 保存按钮名称
},
// detail-panel 表单相关配置
detailPanel: {
mainKey: '', // 主键字段
title: '', // 详情表单标题
},
}
// ... 支持用户动态扩展
}
}] // ... 更多菜单
}
elpis将组件的注册流程扩展了出去,开发者可以自定义组件并且在DSl中去配置和使用。
封装npm
elpis的初衷是作为npm包去供开发人员使用,因此将整个核心进行了抽离,封装为npm包,通过webpack alias、app属性挂载、约定式文件等方式,将核心流程精细化的暴露给开发者。
思考和展望
elpis是抛砖引玉的一个过程,随着框架的不断发展和成熟,会有更多的组件被沉淀出来,我们后续可以将组件部分抽离成物料组件库,提供各种物料给开发者,真正做到配置即使用。
结尾
通过elpis,你可以快速搭建一整套系统,对于重复性的页面可以快速开发,对于非常规页面可以按原风格开发,真正做到了
沉淀80%,定制化20%的页面。如果你也对该项目很感兴趣,抖音搜索哲玄前端,完成学习任务可成为开发小组成员,期待你的加入!👀。