基于DSL+BFF的全栈框架Elpis学习心得

87 阅读15分钟

本篇为个人学习心得,内容来源:抖音“前端哲玄”

背景

随着前端技术的日益成熟,开发工作中的重复性任务比例显著上升,CRUD操作逐渐成为开发人员的主要工作内容。这种"体力活"占比过高的现象,不仅降低了开发效率,也影响了工作创造性。为解决这一行业共性痛点,基于领域模型的企业级全栈开发框架Elpis应运而生,旨在通过技术手段提炼并自动化通用功能开发。

核心架构

作为基于领域模型的全栈框架,Elpis的核心围绕两个关键点展开:通用功能抽象化(解决重复劳动)和自主数据处理(实现全栈能力),具体体现为DSL(领域特定语言)和BFF(面向前端的后端)层。

DSL - “灵魂”

什么是DSL

DSL(Domain-Specific Language),即领域特定语言。与Java、Python这类通用编程语言(GPL)不同,DSL是为解决特定领域问题而专门设计的计算机语言。 在低代码上下文中,DSL就是用来描述应用程序、业务流程、数据模型和用户界面的专用语言。它是用户(开发者)与低代码引擎之间的“契约”或“蓝图”。

表现形式

它通常不表现为传统的文本代码,而是更友好的形式:

  1. 可视化设计器(Visual Designer):这是最常见的形式。当你在画布上拖拽组件、配置属性、绘制流程图时,平台在后台正在生成和维护一份结构化的DSL数据。
  2. JSON/YAML等结构化数据:许多平台的底层配置最终都会序列化为JSON或YAML格式。这份数据完整定义了应用的状态。
  3. 自然语言或类SQL语法:在一些高级场景中,可能会用于定义业务规则或数据查询。

这里仅作介绍,后文会详细介绍DSL模型的建设过程。

核心作用

对于一众类似Elpis的低代码开发平台而言,DSL 的核心作用和优势基本包含下面几个方面:

  1. 抽象与封装: 它将复杂的底层技术(如DOM操作、HTTP请求、状态管理)封装成简单的、业务友好的概念(如“按钮”、“表单”、“提交”)。用户无需关心如何用React/Vue实现一个按钮,只需关心其业务属性。
  2. 跨平台输出: DSL是“中立的”,它只描述“做什么”,而不是“如何做”。同一个描述按钮的DSL,可以被低代码引擎的解释器(Interpreter) 翻译成不同技术栈的代码:用于Web端可以输出为React组件,用于移动端可以输出为React Native或小程序组件,甚至用于后端可以生成API接口定义。
  3. 实现可视化开发: 可视化编辑器本质是一个 “DSL编辑工具” 和 “实时预览工具” 的结合体。你看到的图形界面是对DSL的可视化渲染,你的操作是对DSL数据的修改。
  4. 保证规范与协作: DSL强制开发者按照平台预定义的模式和规范来构建应用,减少了代码的随意性和不一致性,降低了维护成本,使得业务专家和开发者能在同一套语境下协作。

Elpis的DSL以可视化设计器为主要交互方式,底层采用JSON等结构化数据格式,实现了三大核心价值: 技术抽象:将复杂的技术实现封装为业务友好的概念 跨平台能力:通过解释器实现多端代码输出 协作标准化:建立统一的开发规范,降低协作成本

BFF层

什么是BFF?

BFF(Backend For Frontend),即服务于前端的后端。它是一种架构模式,由ThoughtWorks提出,指在前后端之间专门为某一类用户界面(如Web、App、小程序)定制一个中间层后端。

Elpis为什么必须要有BFF?

低代码平台的核心承诺是“全栈”,这意味着它必须能处理前端UI和后端逻辑。BFF层在这里扮演了无可替代的角色:

  1. 聚合与裁剪: 一个企业应用可能需要从多个微服务、数据库、第三方API获取数据。BFF层作为中间件,统一调用这些下游服务,将数据聚合、过滤、转换成一个最适合前端UI渲染的格式。

示例:一个“用户仪表盘”页面需要显示用户信息、订单列表和消息通知。BFF会分别调用用户服务、订单服务和消息服务,将三个接口的数据合并成一个JSON对象,一次性返回给前端。避免了前端发起多次请求的复杂性。

  1. 实现后端逻辑: 低代码并非只能做界面。真正的全栈低代码允许用户通过可视化编排或配置的方式实现后端业务逻辑(如权限校验、工作流审批、数据计算、定时任务)。 BFF层就是这些逻辑的运行时环境。用户配置的“当表单提交时,先校验权限,再更新数据库A和B,最后发送一条消息”这个流程,最终就是在BFF层被执行的。
  2. 安全边界: BFF层隔绝了前端与敏感的后端微服务和数据库。前端只与BFF通信,而BFF持有访问下游服务的凭证和权限。这大大减少了攻击面。 它可以在这一层统一实现身份认证、速率限制、数据脱敏等安全策略。

BFF层在Elpis中承担着关键的数据处理职责,其必要性体现在三个方面: 数据聚合:统一处理多源数据,优化前端数据获取 逻辑执行:作为后端业务的运行时环境 安全边界:构建前后端之间的安全隔离层

解析引擎

现在我们了解了Elpis的两个核心点,已经有了前端页面的模板文件DSL文档,和处理所有数据逻辑的BFF层,接下来我们需要让二者逐步地连接起来。

第一步,我们需要正确解析BFF层文件,让我们的项目“活”起来,先来梳理需要解析的结构:

  • 项目配置文件------------config-----------------configLoader
  • 三方扩展程序------------extend----------------extendLoader
  • 服务端逻辑处理程序---service----------------serviceLoader
  • 请求交互程序------------controller-------------controllerLoader
  • 中间件---------------------middleware----------middlewareLoader
  • 路由校验------------------router-schema------routerSchemaLoader
  • 路由管理------------------router------------------routerLoader

相应的,我们就需要分别将这些功能结构解析并加载到项目的实例中,于是有了我们的核心解析器Elpis-core

在这里插入图片描述

Elpis-core作为核心解析器,采用模块化设计,分别处理配置文件、扩展程序、服务逻辑等七大功能模块,确保框架的灵活性和可扩展性。

工程化

实现核心解析程序后,我们的基础建设告一段落,也会迎来我们的第一次”程序打包“,那么我们就需要对打包过程进行优化,也就是——工程化

我们需要使用各式各样的loader来对我们的文件进行处理,使得浏览器能够正确地识别解析不同文件,包括但不限于:

  • vue-loader———.vue
  • babel-loader——.js
  • css-loader———.css | .less
  • less-loader———.less
  • style-loader——.css | .less
  • url-loader———.png | .jpg | .jepg | .gif
  • file-loader———.eot | .svg | .ttf | .woff | .woff2

随着开发地不断深入进行,我们的代码体积会日益增大,精细化的代码分割就显得尤为重要:

// 打包输入优化(代码分割、模块合并、缓存、Treeshaking、压缩等优化策略)
  optimization: {
    splitChunks: {
      /**
       * 把 js 文件打包为 3 类
       * 1. vender: 第三方库,基本不会改动,除非依赖版本升级
       * 2. common:业务组件代码的公共部分抽取,改动较少
       * 3. entry.{page}: 不同页面 entry 中的业务组件代码的差异部分,经常改动
       * 将改动和引用频率不同的 js 进行区分,充分利用浏览器的缓存机制
       */
      chunks: 'all', // 所有的 chunks 都进行分割(同步、异步)
      maxAsyncRequests: 10, // 每次异步加载 最多请求 5 个文件 (最大并行数)
      maxInitialRequests: 10, // 首次加载时 最多请求 5 个文件 (最大并行数)
      cacheGroups: {
        vender: { // 第三方依赖库
          test: /[\\/]node_modules[\\/]/,
          name: 'vender',
          priority: 10, // 优先级,越大优先级越高
          enforce: true, // 强制执行
          reuseExistingChunk: true // 重复引用时,只打包一次
        },
        common: { // 业务组件代码的公共部分
          test: /[\\/]common|widgets[\\/]/,
          name: 'common',
          minChunks: 2, // 被 2 处引用,才会打包到 common 中
          minSize: 1, // 最小分割文件大小 (单位 字节)
          priority: 5,
          reuseExistingChunk: true
        }
      }
    },
    // 将 webpack 运行时 的运行时代码单独打包,减少代码体积
    runtimeChunk: true
  }

作为程序开发人员,我们需要将程序运行在不同环境:开发环境生产环境,那么意味着我们的打包需要差异化

开发环境

开发环境下,我们需要频繁地进行代码修改与效果查看,热更新显得尤为重要,通过本地的资源服务器对文件进行监听,当监听到修改后通知客户端进行代码更新,并重新进行渲染。 在这里插入图片描述

生产环境

生产环境打包配置的核心目标是:

  • 速度优化:多线程、缓存、并行处理
  • 体积优化:压缩、Tree Shaking、代码分割
  • 缓存优化:内容哈希、持久化缓存
  • 体验优化:动态导入、懒加载
plugins: [
    // build 前清空 dist 目录
    new CleanWebpackPlugin(['public/dist'], {
      root: path.resolve(process.cwd(), './app/'),
      exclude: [],
      verbose: true,
      dry: false
    }),
    // 提取公共 css 部分,有效利用缓存
    new MiniCssExtractPlugin({
      chunkFilename: 'css/[name]_[chunkhash:8].bundle.css'
    }),
    // 优化并压缩 css 资源
    new CSSMinimizerPlugin(),
    // 多线程打包 js,加快打包速度
    new HappyPackPlugin({
      ...happypackCommonConfig,
      id: 'js',
      loaders: [ `${require.resolve('babel-loader')}?${JSON.stringify({
        presets: [ require.resolve('@babel/preset-env') ],
        plugins: [ require.resolve('@babel/plugin-transform-runtime') ]
      })}` ]
    }),
    // 多线程打包 css,加快打包速度
    new HappyPackPlugin({
      ...happypackCommonConfig,
      id: 'css',
      loaders: [{
        path: require.resolve('css-loader'),
        options: {
          importLoaders: 1
        }
      }]
    }),
    //  浏览器请求资源时,不发送用户的身份凭证
    new HtmlWebpackInjectAttributesPlugin({
      crossorigin: 'anonymous'
    })
  ],
  optimization: {
    // 使用 TerserPlugin 的并发和缓存,提升压缩阶段的性能
    // 清除所有 console.log
    minimize: true,
    minimizer: [
      new TerserPlugin({
        cache: true, // 启用缓存来加速构建
        parallel: true, // 利用多核 CPU 的优势来加快压缩速度
        terserOptions: {
          compress: {
            drop_console: true // 删除所有 console.log
          }
        }
      })
    ]
  }

框架的工程化建设涵盖开发和生产双环境优化: 开发环境:通过热更新机制提升开发体验 生产环境:采用多线程打包、代码分割等优化策略,确保运行性能

领域模型建设

在文章的开始,我们了解到DSL是对程序开发的抽象,让用户专注于功能而非实现,同时也了解到存在DSL描述文档,同时需要存在DSL解析器来帮助我们完成文档UI界面的转化。 在这里插入图片描述 基于我们的初衷——减少重复打螺丝流程、将重心放在独立业务功能,我们需要对项目的功能进行分类,大致如下:

  • Schema:标准化页面,即抽离的高相似复用模板页面
  • Iframe:内嵌三方页面
  • Sider:侧边栏页面
  • Custom:用户自定义页面,即独立业务页面

下面是Elpis中的一个DSL文档范例:

# 菜单栏
	# 标签页1
		# 标签页1名称
		# 菜单类型 (module 模块)
		# 页面类型 (schema 标准化)
		# schema 配置
			# api 数据源 (页面由数据驱动) (遵循 RESTFUL 规范)
			# 板块数据结构
				# 表格列字段 1
					# 字段类型
					# 字段类型
					# 表格列配置
					# 搜索区配置
					# 操作表单配置
					# ...
				# 表格列字段 2
					# ...
				# 表格列字段 3
					# ...
		# 表格统一配置
		# 搜索区配置
		# 模块组件配置
		# ...

转化为Elpis可以解析的JSON结构即为

module.exports = {
  model: 'dashboard',
  name: '电商系统',
  menu: [{
    key: 'product',
    name: '商品管理',
    menuType: 'module',
    moduleType: 'schema',
    schemaConfig: {
      api: '/api/proj/product',
      schema: {
        title: 'Product',
        descriprtion: "product's properties and they styles in table or other section",
        type: 'object',
        properties: {
          product_id: {
            type: 'string',
            label: '商品ID',
            tableOptions: {
              width: 150,
              'show-overflow-tooltip': true
            }
          },
          product_name: {
            type: 'string',
            label: '商品名称',
            tableOptions: {
              width: 200
            },
            searchOptions: {
              comType: 'dynamicSelect',
              api: '/api/proj/product_enum/list'
            }
          },
          price: {
            type: 'number',
            label: '价格',
            tableOptions: {
              width: 200
            },
            searchOptions: {
              comType: 'select',
              enumList: [{
                label: '全部',
                value: -1
              }, {
                label: '¥11.11',
                value: 11.11
              }, {
                label: '¥22.22',
                value: 22.22
              }, {
                label: '¥33.33',
                value: 33.33
              }]
            }
          },
          inventory: {
            type: 'number',
            label: '库存',
            tableOptions: {
              width: 200
            },
            searchOptions: {
              comType: 'input',
            }
          },
          create_time: {
            type: 'string',
            label: '创建时间',
            tableOptions: {
              'show-overflow-tooltip': true
            },
            searchOptions: {
              comType: 'dateRange'
            }
          }
        }
      },
      tableConfig: {
        headerButtons: [{
          label: '新增商品',
          type: 'primary',
          plain: true,
          eventKey: 'showComponent'
        }],
        rowButtons: [{
          label: '修改',
          type: 'warning',
          eventKey: 'showComponent'
        }, {
          label: '删除',
          type: 'danger',
          eventKey: 'remove',
          eventOptions: {
            params: {
              product_id: 'schema::product_id'
            }
          }
        }]
      }
    }
  }, {
    key: 'order',
    name: '订单管理',
    menuType: 'module',
    moduleType: 'custom',
    customConfig: {
      path: '/todo'
    },
  }, {
    key: 'client',
    name: '客户管理',
    menuType: 'module',
    moduleType: 'custom',
    customConfig: {
      path: '/todo'
    },
  }]
}

动态组件

动态组件指的是那些不在编译时静态引入,而是在运行时根据数据(如DSL Schema)动态决定加载哪个组件的组件。

例如在Elpis中,Schema模板页面里的“弹出框组件”就是一个典型例子:它的类型、属性、甚至内容都不是在代码里固定写死的,而是由DSL配置动态生成的。

这里简单说明动态组件的必要性:

  • 实现真正的配置化驱动: 支持用户通过配置DSL就能创建复杂交互,实现“零代码”或“低代码”
  • 支撑Schema模板的灵活性和复用性:根据数据状态动态显示不同的组件组合
  • 实现Custom页面的渐进式复杂度:尽量使用动态组件减少重复功能开发

下面是DSL中配置动态组件的范例:

{
    key: "points",
    name: "点位管理",
    menuType: "module",
    moduleType: "schema",
    schemaConfig: {
      api: "/api/proj/point",
      schema: {
        type: "object",
        properties: {
          index: {
            type: "number",
            label: "序号",
            tableOption: {
              visible: true,
              width: 60,
              align: 'center'
            }
          },
          id: {
            type: "string",
            label: "ID",
            tableOption: {
              visible: true,
              width: 60,
              align: 'center'
            },
            detailPanelOption: {}
          },
          title: {
            type: "string",
            label: "点位标题",
            tableOption: {
              visible: true
            },
            detailPanelOption: {}
          },
          point_lon: {
            type: "string",
            label: "点位经度",
            tableOption: {
              visible: true
            },
            detailPanelOption: {}
          },
          point_lat: {
            type: "string",
            label: "点位纬度",
            tableOption: {
              visible: true
            },
            detailPanelOption: {}
          },
          create_time: {
            type: "string",
            label: "创建时间",
            tableOption: {
              visible: true
            },
            detailPanelOption: {}
          },
          update_time: {
            type: "string",
            label: "更新时间",
            tableOption: {
              visible: true
            },
            detailPanelOption: {}
          }
        }
      },
      tableConfig: {
        rowButtons: [{
          label: '详情',
          eventKey: 'showComponent',
          eventOption: {
            comName: 'detailPanel'
          },
          type: 'primary'
        }]
      },
      componentConfig: {
        detailPanel: {
          mainKey: 'id',
          title: '点位详情'
        }
      }
    }
  }

其中componentConfig部分即为标准Schema页面中所配置的详情信息弹出框。

Elpis通过四类页面模板(Schema、Iframe、Sider、Custom)实现业务场景全覆盖,其中动态组件技术是实现配置化驱动的关键,支持运行时根据DSL配置动态加载组件。

Npm发布

至此我们已经完成了前后端全部囊括在内的基于DSL领域模型的低代码全栈框架Elpis,为了方便后续我们的使用方便,可以将我们开发的这套框架在npm上进行发布,同样可以进一步编写一个脚手架来帮助我们一键式初始化项目框架。

下面简要介绍脚手架开发及npm发布流程。

Elpis-cli

这里是脚手架程序目录结构

elpis-cli/
├── README.md                     # 项目说明文档
├── bin/                          # 可执行文件目录
│   └── cli.js                    # 命令行入口文件
├── package-lock.json             # npm 依赖锁定文件
├── package.json                  # 项目配置和依赖管理
└── templates/                    # 项目模板目录
    └── default/                  # 默认模板
        ├── app/                  # 应用程序目录
        │   ├── controller/       # 控制器目录
        │   │   └── demo.js       # 示例控制器
        │   ├── extend/           # 扩展目录
        │   │   └── demo.js       # 示例扩展
        │   ├── middleware/       # 中间件目录
        │   │   └── demo.js       # 示例中间件
        │   ├── middleware.js     # 中间件配置
        │   ├── pages/            # 页面目录
        │   │   ├── assets/       # 静态资源
        │   │   ├── common/       # 公共组件
        │   │   ├── dashboard/    # 仪表盘页面
        │   │   ├── project-list/ # 项目列表页面
        │   │   ├── store/        # 状态管理
        │   │   └── widgets/      # 小部件
        │   ├── router/           # 路由目录
        │   │   └── demo.js       # 示例路由
        │   ├── router-schema/    # 路由模式目录
        │   │   └── demo.js       # 示例路由模式
        │   ├── service/          # 服务目录
        │   │   └── demo.js       # 示例服务
        │   └── webpack.config.js # Webpack配置
        ├── build.js              # 构建脚本
        ├── config/               # 配置目录
        │   ├── config.beta.js    # 测试环境配置
        │   ├── config.default.js # 默认配置
        │   ├── config.local.js   # 本地开发配置
        │   └── ocnfig.prod.js    # 生产环境配置
        ├── model/                # 数据模型目录
        │   └── demo/             # 示例模型
        │       ├── model.js      # 模型定义
        │       └── project/      # 项目模型子目录
        └── server.js             # 服务器入口文件

这个脚手架程序采用了模块化的结构设计,通过其中 templates 目录提供项目模板,使用 bin/cli.js 作为命令行入口,可以帮助使用者快速初始化具有完整前后端结构的项目。

发布

  • 前置操作:剥离业务代码框架实现代码
  • 注册npm账号
  • 重置npm镜像源地址
  • 登录npm账号
  • 确认npm账号
  • 发布
npm config get
npm config set registry

npm login

npm whoami

npm publish --access public
npm publish 

通过脚手架工具和npm发布,初步为Elpis建立开发生态,支持项目快速初始化和标准化部署。

总结

通过对Elpis全栈框架的深入学习和实践,可以认识到现代企业级开发框架的设计哲学已从单纯的技术堆砌转向了关注开发效率和维护性的新阶段。

Elpis通过DSL领域模型实现了业务逻辑与技术实现的解耦,让开发者能够专注于业务创新而非重复劳动。其BFF层的精巧设计不仅解决了前后端协作的效率瓶颈,更构建了安全可靠的数据流转通道。在工程化方面,差异化的环境配置和优化策略展现了框架对开发体验和运行性能的双重追求。特别值得称道的是动态组件机制,这一设计使得框架既保证了标准化开发的效率,又保留了应对复杂业务场景的灵活性。从技术架构到工具链建设,Elpis体现了一个成熟框架应有的完整性和前瞻性。

Elpis的设计,通过合理的抽象和自动化,致力于将开发者从繁琐的重复劳动中解放出来,真正释放技术创新的潜力。

夹带私货: 学前端,看抖音《哲玄前端》