Elpis 五个里程碑总结

7 阅读11分钟

Elpis 架构演进:五个里程碑

本文总结 Elpis 框架从 0 到 1 的完整架构演进过程,涵盖服务端内核、前端工程化、领域模型、动态组件库、npm 包封装五个核心阶段。


一、Node.js 服务端内核引擎

第一阶段的核心任务是构建一个稳定、可扩展的服务端内核引擎 elpis-core,基于 Koa2 实现。核心设计理念是"框架优先,业务扩展"——通过 Loader 机制实现框架代码和业务代码的自动加载与整合。

Loader 自动加载机制:框架与业务的整合

每个 Loader 都遵循"先框架,后业务"的加载顺序:先扫描框架目录(如 elpis-core 下的 app/controller),再扫描业务项目目录(如业务项目根目录下的 app/controller),将两边的文件统一处理并挂载到同一个对象上(如 app.controller)。文件路径会转为驼峰命名,支持多级目录(如 custom-module/custom-controller.js 对应 app.controller.customModule.customController)。这种机制带来三个价值:框架和业务可以并存;业务同名文件会覆盖框架;业务可以扩展任意新文件,无需改框架。serviceLoader、routerLoader、middlewareLoader 等均采用同样的"框架+业务"整合模式。

业务项目的使用方式

业务项目只需在约定的目录结构下编写代码,框架会自动加载:

业务项目/
├── app/
│   ├── controller/
│   │   └── user.js          # 自动加载到 app.controller.user
│   ├── service/
│   │   └── user.js          # 自动加载到 app.service.user
│   ├── router/
│   │   └── user.js          # 自动注册路由
│   └── router-schema/
│       └── user.js          # 自动加载校验规则

业务开发者无需关心加载逻辑,框架在 start 时会自动扫描并加载这些文件。

router 和 router-schema 的解耦联动

Elpis 将路由映射(router)和参数校验(router-schema)拆开管理,再通过中间件串联。业务在 app/router/ 下定义「路径 → controller 方法」的映射,在 app/router-schema/ 下用 JSON Schema 声明各路径、各方法的参数规则(如 body、query 的 type、required 等)。router-schema 由 router-schemaLoader 加载后挂到 app.routerSchema,结构按「路径 → 方法 → schema」组织。请求进入时,api-params-verify 中间件根据 ctx.pathctx.methodapp.routerSchema 里取对应 schema,用 ajv 校验通过后再放行到 router 匹配到的 controller,校验不通过则直接返回 400。这样路由只负责「谁处理」,schema 只负责「参数长什么样」,职责分离;校验在中间件层统一完成,Controller 无需手写校验逻辑,且能信任已通过校验的入参。

通过 Loader 机制与 router-schema 解耦设计,Elpis 实现框架与业务解耦:业务在约定目录下编写 router、router-schema 等,框架负责加载、整合与关联。


二、Webpack5 工程化建设

第二阶段的核心任务是构建前端工程化体系,基于 Webpack5 实现多环境构建、热更新、业务扩展等能力。

2.1 Alias 机制:框架与业务的协同扩展

Alias 机制不仅仅是简化路径引用,更重要的是实现了框架与业务的协同扩展

框架能力向业务开放

Elpis 通过 Webpack 的 resolve.alias 配置,将框架内置的组件、工具、配置暴露给业务项目。业务代码可以直接引用框架内部模块,而无需关心实际的物理路径:

// 业务代码中引用框架组件
import HeaderContainer from '$elpisHeaderContainer';
import $curl from '$elpis$curl';

这种设计的核心价值在于:框架升级时,只需调整 Webpack 配置,业务代码无需修改。框架的内部结构可以自由重构,对外暴露的别名保持稳定,确保了向下兼容性。

业务扩展配置的动态注入机制

框架通过检测业务项目中是否存在特定的配置文件,动态决定使用业务配置还是空白模块。

例如:

  • $businessDashboardRouterConfig:检测 ./app/pages/dashboard/router.js,允许业务自定义 dashboard 路由配置
  • $businessFormItemConfig:检测 ./app/pages/widgets/schema-form/form-item-config.js,允许业务扩展自定义表单项组件

实现机制:空白文件替代法

核心代码示例(webpack.base.js:142-146):

const businessDashboardRouterConfig = path.resolve(
  process.cwd(),
  './app/pages/dashboard/router.js'
);
aliasMap['$businessDashboardRouterConfig'] = fs.existsSync(
  businessDashboardRouterConfig
)
  ? businessDashboardRouterConfig
  : blankModulePath;

这种**"存在则扩展,不存在则忽略"**的机制:

  • 使用 fs.existsSync() 动态检测业务文件是否存在
  • 存在时,alias 指向业务文件,框架使用业务的扩展配置
  • 不存在时,alias 指向 blankModulePath(空白 JS 文件:module.exports = {}),Webpack 打包时不会报"模块未找到"错误

这种设计实现了渐进式扩展:业务项目可以从完全使用框架默认配置开始,逐步根据需要添加自定义配置文件,无需一开始就提供完整的配置。框架代码在导入这些别名时,会判断是否为有效配置,从而决定是否应用业务扩展。

包处理规则:require.resolve() 引用声明

在处理第三方依赖时,我们采用了动态解析策略。Webpack 配置中使用 require.resolve() 来声明包的引用路径:

resolve: {
  alias: {
    'vue': require.resolve('vue'),
    'element-plus': require.resolve('element-plus')
  }
}

这样做的好处是确保所有模块引用的是同一个包实例,避免了多版本共存导致的奇怪问题(特别是 Vue、React 这类状态管理框架)。require.resolve() 会根据 Node.js 的模块解析规则,找到 node_modules 中实际安装的包路径。

2.2 多环境差异构建

Elpis 通过独立的配置文件实现多环境构建,应对开发和生产环境的不同需求。

配置文件结构

Webpack 配置采用三层结构:

  • webpack.base.js:基础配置,包含 entry、alias、loader、plugin 等所有环境共享的配置,以及 Code Splitting 策略(将代码分为 vendor、common、entry 三层,利用浏览器缓存)
  • webpack.dev.js:开发环境配置,使用 webpack-merge 合并 base 配置
  • webpack.prod.js:生产环境配置,使用 webpack-merge 合并 base 配置

开发环境特点

开发环境优先考虑开发效率和调试便利性:

// webpack.dev.js
mode: 'development',
devtool: 'eval-cheap-module-source-map',  // 快速的 source-map
output: {
  publicPath: 'http://127.0.0.1:9002/public/dist/dev/'  // 指向开发服务器
},
plugins: [
  new webpack.HotModuleReplacementPlugin()  // 热更新插件
]

关键配置:

  • 开启详细的 source-map,方便定位代码问题
  • 配置热更新插件(HotModuleReplacementPlugin)
  • Entry 注入 HMR 客户端代码,建立 WebSocket 通信
  • 输出路径指向开发服务器的端口

生产环境特点

生产环境优先考虑性能和产物优化:

// webpack.prod.js
mode: 'production',
optimization: {
  minimize: true,
  minimizer: [
    new TerserWebpackPlugin({
      terserOptions: {
        compress: { drop_console: true }  // 移除 console.log
      }
    })
  ]
},
plugins: [
  new CleanWebpackPlugin(),           // 清理旧文件
  new MiniCssExtractPlugin(),         // CSS 提取
  new CssMinimizerPlugin()            // CSS 压缩
]

关键配置:

  • 使用 TerserPlugin 压缩 JS,移除 console.log
  • 使用 thread-loader 并行处理 JS 和 CSS,提升构建速度
  • 提取并压缩 CSS 文件
  • 关闭性能提示(performance.hints: false),避免构建时的警告信息

环境切换

框架提供了两个独立的启动脚本:

  • app/webpack/dev.js:开发环境启动脚本,直接引用 webpack.dev.js 配置并启动开发服务器
  • app/webpack/prod.js:生产环境构建脚本,直接引用 webpack.prod.js 配置执行构建
// app/webpack/dev.js
const { webpackConfig, DEV_SERVER_CONFIG } = require('./config/webpack.dev');

// app/webpack/prod.js
const webpackProdConfig = require('./config/webpack.prod.js');

通过调用不同的脚本文件实现环境切换,各环境独立启动,职责清晰。

2.3 自研热更新服务

Elpis 框架采用服务端渲染架构,前端构建后生成模板文件(.tpl)交给后端 Koa 服务渲染。这种架构对开发环境的热更新提出了特殊要求。

架构特殊性

在开发过程中需要同时满足:

  • 前端代码改动后即时看到效果(热更新)
  • 模板文件必须写入磁盘,供后端 Koa 服务读取

传统的 webpack-dev-server 将所有文件保存在内存中,无法满足模板落盘的需求。同时,前端构建服务(9002 端口)和后端 Koa 服务(8080 端口)分离,涉及跨域资源加载。

自研方案实现

基于以上需求,我们选择了 Express + webpack 中间件的自研方案(位于 app/webpack/dev.js):

const express = require('express');
const webpack = require('webpack');
const devMiddleware = require('webpack-dev-middleware');
const hotMiddleware = require('webpack-hot-middleware');

const app = express();
const compiler = webpack(webpackConfig);

// 开发中间件:监听文件变化、触发编译
app.use(devMiddleware(compiler, {
  writeToDisk: (filepath) => filepath.endsWith('.tpl'),  // 仅模板文件落盘
  publicPath: webpackConfig.output.publicPath,
  headers: {
    'Access-Control-Allow-Origin': '*'  // 跨域支持
  }
}));

// 热更新中间件:WebSocket 通信
app.use(hotMiddleware(compiler, {
  path: '/__webpack_hmr'
}));

app.listen(9002);

关键配置

writeToDisk 选择性落盘

  • 模板文件(.tpl)写入磁盘,供后端读取
  • JS、CSS 等资源保留在内存,加快热更新速度

跨域头配置

  • 前端资源从 9002 端口加载
  • 页面在 8080 端口渲染
  • 需要设置 CORS 头允许跨域访问

HMR 通信机制

  • webpack.dev.js 在 entry 中注入 HMR 客户端代码
  • 客户端通过 WebSocket 连接到 http://127.0.0.1:9002/__webpack_hmr
  • 文件变化时,服务端推送更新消息,客户端动态替换模块

2.4 业务自定义 Webpack 配置

Elpis 允许业务项目在不修改框架 webpack 配置的情况下,添加自己的构建规则。

扩展机制

框架在加载 webpack 配置时,会尝试读取业务项目的自定义配置:

// webpack.base.js (59-68行)
let businessWebpackConfig = {};
try {
  businessWebpackConfig = require(path.resolve(
    process.cwd(),
    './app/webpack.config.js'
  ));
} catch (error) {
  console.log('[exception] there is no business webpack config file.');
}

// 合并配置
module.exports = merge.smart(baseConfig, businessWebpackConfig);

扩展方式

业务项目可以创建 ./app/webpack.config.js 文件,添加自定义配置:

  • 添加新的 loader(如处理特殊文件格式)
  • 添加新的 plugin(如代码分析、性能监控)
  • 扩展 alias 别名
  • 修改 optimization 优化策略

使用 webpack-mergesmart 模式进行配置合并,数组类型的配置会合并,对象类型的配置会深度合并。


三、领域模型架构建设

前两个阶段解决了"服务端内核"和"前端工程化",第三阶段的核心是业务建模能力

设计理念:80%共性 20%定制化

在管理后台开发中,存在大量标准化、重复性的工作,这些工作技术含量不高但很费时间,每个项目都要做一遍:

80%共性(框架通过配置解决):

  • 展示列表:数据表格展示
  • 搜索数据:搜索栏、筛选条件
  • 新增编辑:表单录入、字段校验
  • 数据校验:必填项、格式验证
  • 按钮权限控制:增删改查权限

通过统计,管理后台项目里,这类工作占了 80% 的开发时间。Elpis 通过 DSL 配置 + 解析引擎将这些标准功能抽象出来,开发者只需编写配置文件,框架自动渲染对应的 UI 和交互逻辑。

20%定制化(框架提供扩展接口):

  • 业务特有逻辑:订单系统的复杂状态流转、库存系统的批量导入导出
  • 个性化交互:特殊的图表展示、流程图、复杂的状态联动
  • 定制化组件:业务特有的表单控件、展示组件

这些是每个业务不一样的,没法完全通用化。框架通过扩展机制(自定义组件、自定义页面、业务 hook)留接口让开发者自己实现。

划分标准:能不能复用。能复用的、有规律的,就归到 80%,用配置解决;不能复用的、逻辑复杂的,就归到 20%,留接口自己实现。

举例

  • 用户管理功能每个系统都有,这是 80%,通过 Model 配置即可实现
  • 某个系统要求用户注册前必须人脸识别,这就是 20%,需要开发者自定义实现

这种设计带来的价值:

  • 开发效率提升:80% 的标准功能无需编码,配置即可完成
  • 质量保障:框架统一处理标准逻辑,减少重复开发带来的 bug
  • 灵活性:20% 的定制化空间确保框架能适应各种复杂业务场景

3.1 Model 配置:统一的领域模型定义

Elpis 通过 Model 配置描述整个应用的业务领域模型,包括菜单结构、数据模型、UI 交互等所有要素,是实现"80%共性"的核心工具。

Model 配置结构

{
  mode: 'dashboard',        // 模板类型
  name: '项目名称',
  menu: [                   // 菜单配置
    {
      key: 'user-mgmt',
      name: '用户管理',
      menuType: 'module',   // 枚举:group / module
      moduleType: 'schema', // 枚举:sider / iframe / schema / custom
      schemaConfig: {
        api: '/api/user',   // RESTFUL API
        schema: {           // 字段定义
          type: 'object',
          properties: {
            id: {
              type: 'string',
              label: '用户ID',
              tableOption: { visible: true, width: 120 },
              searchOption: { comType: 'input' },
              createFormOption: { visible: false },
              editFormOption: { visible: false }
            },
            name: {
              type: 'string',
              label: '用户名',
              tableOption: { visible: true },
              searchOption: { comType: 'input' },
              createFormOption: { comType: 'input', required: true },
              editFormOption: { comType: 'input', required: true }
            }
          }
        },
        tableConfig: {        // 表格配置
          headerButtons: [...],
          rowButtons: [...]
        },
        searchConfig: {},     // 搜索栏配置
        componentConfig: {    // 动态组件配置
          createForm: { title: '创建用户' },
          editForm: { title: '编辑用户', mainKey: 'id' }
        }
      }
    }
  ]
}

Model 配置的特点

通过一个统一的配置,定义:

  • 菜单结构(menuType、moduleType)
  • 字段模型(schema.properties)
  • 各场景展示方式(xxxOption)
  • 交互逻辑(tableConfig、componentConfig)

框架解析配置后,自动生成完整的 CRUD 页面,开发者无需编写任何组件代码。

3.2 DSL 解析引擎:动态提取场景化 Schema

框架提供了 DSL 解析引擎,根据组件类型从统一的 schema 中动态提取对应的配置。

解析引擎核心实现

位于 app/pages/dashboard/complex-view/schema-view/hooks/schema.js

// 通用构建 schema 方法(清除噪音)
const buildDtoSchema = function (_schema, comName) {
  const dtoSchema = {
    type: 'object',
    properties: {},
  };

  // 提取有效 schema 字段信息
  for (const key in _schema.properties) {
    const props = _schema.properties[key];
    if (props[`${comName}Option`]) {
      let dtoProps = {};
      // 提取 props 中非 option 的部分
      for (const pKey in props) {
        if (pKey.indexOf('Option') < 0) {
          dtoProps[pKey] = props[pKey];
        }
      }
      // 合并 comName Option
      dtoProps = Object.assign({}, dtoProps, {
        option: props[`${comName}Option`],
      });
      dtoSchema.properties[key] = dtoProps;
    }
  }

  return dtoSchema;
};

解析过程

  1. 输入:完整的 schema 配置 + 组件名称(如 'table''search''createForm'
  2. 过滤:遍历所有字段,仅提取包含对应 xxxOption 的字段
  3. 转换:将字段基础信息(type、label)+ 对应的 Option 合并为该组件的专属 schema
  4. 输出:针对特定组件优化的精简 schema

示例:从统一 schema 提取不同组件的 schema

原始 schema 定义:

{
  properties: {
    id: {
      type: 'string',
      label: '用户ID',
      tableOption: { visible: true, width: 120 },
      searchOption: { comType: 'input' }
    },
    name: {
      type: 'string',
      label: '用户名',
      tableOption: { visible: true },
      searchOption: { comType: 'input' },
      createFormOption: { comType: 'input', required: true }
    }
  }
}

经过 buildDtoSchema(_schema, 'table') 提取后:

{
  properties: {
    id: {
      type: 'string',
      label: '用户ID',
      option: { visible: true, width: 120 }
    },
    name: {
      type: 'string',
      label: '用户名',
      option: { visible: true }
    }
  }
}

经过 buildDtoSchema(_schema, 'createForm') 提取后:

{
  properties: {
    name: {                    // id 字段被过滤掉(无 createFormOption)
      type: 'string',
      label: '用户名',
      option: { comType: 'input', required: true }
    }
  }
}

解析引擎的价值

  • 按需提取:组件只获取自己需要的字段配置,避免冗余数据传递
  • 自动过滤:字段通过 visible: false 或缺少对应 Option 自动排除
  • 统一接口:所有组件(table/search/form)接收相同结构的 schema,降低组件耦合度

3.3 菜单驱动的页面渲染

Elpis 通过 Model 配置中的 menu 字段驱动整个应用的菜单和页面渲染。

菜单配置支持的类型

menuType 类型

  • group:菜单组,可包含 subMenu 子菜单(支持递归嵌套)
  • module:功能模块,对应具体页面

moduleType 类型(当 menuType 为 module 时):

  • sider:侧边栏模块,包含 siderConfig.menu 二级菜单
  • iframe:内嵌页面,通过 iframeConfig.path 加载外部 URL
  • schema:Schema 驱动的 CRUD 页面,通过 schemaConfig 配置
  • custom:自定义页面,通过 customConfig.path 指定路由路径

菜单查找机制

位于 app/pages/store/menu.js,框架提供了递归查找菜单项的能力:

const findMenuItem = function ({ key, value }, mList = menuList.value) {
  for (let i = 0; i < mList.length; i++) {
    const menuItem = mList[i];
    const { menuType, moduleType } = menuItem;

    if (menuItem[key] === value) {
      return menuItem;
    }

    // 递归查找 group 的 subMenu
    if (menuType === 'group' && menuItem.subMenu) {
      const mItem = findMenuItem({ key, value }, menuItem.subMenu);
      if (mItem) return mItem;
    }

    // 递归查找 sider 的二级菜单
    if (moduleType === 'sider' && menuItem.siderConfig?.menu) {
      const mItem = findMenuItem({ key, value }, menuItem.siderConfig.menu);
      if (mItem) return mItem;
    }
  }
};

页面渲染流程

  1. 路由跳转:用户点击菜单时,通过 onMenuSelect 回调触发路由跳转,携带 key 参数

位于 app/pages/dashboard/dashboard.vue

const onMenuSelect = function (menuItem) {
  const { moduleType, key, customConfig } = menuItem;
  
  // 如果是当前页面,不处理
  if (key === route.query.key) {
    return;
  }
  
  const pathMap = {
    sider: '/sider',
    iframe: '/iframe',
    schema: '/schema',
    custom: customConfig?.path,
  };
  
  router.push({
    path: `/view/dashboard${pathMap[moduleType]}`,
    query: {
      key,
      proj_key: route.query.proj_key,
    },
  });
};
  1. 菜单查找:通过 findMenuItem 在菜单树中递归查找对应的 menuItem
  2. 提取配置:从 menuItem 中提取 schemaConfig(或 iframeConfig、customConfig 等)
  3. 解析渲染
    • 调用 buildDtoSchema 分别构造 tableSchemasearchSchemacomponents 各组件的 schema
    • 将 schema 和 config 传递给对应的动态组件(schema-table、schema-search-bar、schema-form 等)
  4. 组件渲染:动态组件根据 schema 自动渲染 UI

代码位于 app/pages/dashboard/complex-view/schema-view/hooks/schema.js

const buildData = function () {
  const { key, sider_key: siderKey } = route.query;
  
  // 1. 查找菜单项
  const mItem = menuStore.findMenuItem({
    key: 'key',
    value: siderKey ?? key,
  });

  if (mItem && mItem.schemaConfig) {
    const { schemaConfig: sConfig } = mItem;
    const configSchema = JSON.parse(JSON.stringify(sConfig.schema));

    api.value = sConfig.api ?? '';

    // 2. 构造 tableSchema 和 tableConfig
    tableSchema.value = buildDtoSchema(configSchema, 'table');
    tableConfig.value = sConfig.tableConfig;

    // 3. 构造 searchSchema 和 searchConfig
    searchSchema.value = buildDtoSchema(configSchema, 'search');
    searchConfig.value = sConfig.searchConfig;

    // 4. 构造 components(createForm/editForm/detailPanel 等)
    const { componentConfig } = sConfig;
    if (componentConfig && Object.keys(componentConfig).length > 0) {
      const dtoComponents = {};
      for (const comName in componentConfig) {
        dtoComponents[comName] = {
          schema: buildDtoSchema(configSchema, comName),
          config: componentConfig[comName],
        };
      }
      components.value = dtoComponents;
    }
  }
};

3.4 Model 配置的核心地位

在 Elpis 的架构设计中,Model 配置是前后端协作的核心契约,具有极高的重要性。

Model 配置的三重角色

  1. 数据契约:通过 schema 字段定义明确前后端交互的数据结构、字段类型、必填项、取值范围
  2. UI 驱动:通过各种 xxxOption 配置驱动前端组件的自动渲染(表格、表单、搜索栏等)
  3. 业务语义:通过字段的 labeldescription 等传达业务含义,形成团队的"通用语言"

Model 配置驱动的开发流程

在 Elpis 中,Model 配置是开发的起点:

  1. 业务分析阶段:梳理业务需求,编写 Model 配置(定义菜单、字段、交互逻辑)
  2. 前端渲染:框架自动解析 Model 配置,渲染表格、表单、搜索栏等组件
  3. 后端对接:后端根据 Model 中的 api 字段提供 RESTFUL 接口,返回数据格式与 schema 对齐
  4. 联调验证:Model 配置作为验收标准,确保前后端数据结构一致

这种"配置驱动"的理念带来的价值:

  • 快速开发:一个标准的 CRUD 页面,只需编写 Model 配置,无需写任何组件代码
  • 一致性保障:字段定义统一,避免前后端数据结构不一致
  • 易于维护:需求变更时,只需修改 Model 配置,所有相关页面自动同步

Model 配置示例

以用户管理为例,Model 配置的核心结构:

{
  menu: [{
    key: 'user-mgmt',
    name: '用户管理',
    moduleType: 'schema',
    schemaConfig: {
      api: '/api/user',
      schema: {
        properties: {
          name: {
            type: 'string',
            label: '用户名',
            tableOption: { visible: true },              // 表格中显示
            searchOption: { comType: 'input' },          // 搜索栏用输入框
            createFormOption: { comType: 'input', required: true },  // 创建表单必填
            editFormOption: { comType: 'input', required: true }     // 编辑表单必填
          }
        }
      },
      tableConfig: { headerButtons: [...], rowButtons: [...] },
      componentConfig: { createForm: {...}, editForm: {...} }
    }
  }]
}

四、Vue3 动态组件库建设

第四阶段的核心任务是构建基于 JsonSchema 驱动的动态组件库,实现"配置即页面"的能力。

4.1 核心组件体系

Elpis 提供了一套完整的 Schema 驱动的 UI 组件库,位于 app/pages/widgets/ 目录:

核心组件

  • schema-table:表格组件,根据 tableSchema 自动渲染表格列、分页、操作按钮
  • schema-search-bar:搜索栏组件,根据 searchSchema 自动渲染搜索条件
  • schema-form:表单组件,根据 formSchema 自动渲染表单项、校验规则
  • schema-view:综合视图组件,整合 table、search-bar、form 为完整的 CRUD 页面

组件特点

  • Schema 驱动:所有组件通过 JSON Schema 配置自动渲染
  • 可扩展:支持业务自定义组件类型
  • 类型丰富:支持 input、select、date、upload 等常见表单控件

4.2 schema-form:表单项自动渲染

schema-form 根据 schema 配置自动渲染表单项,支持各种 Element Plus 表单控件。

核心能力

  • 根据 option.comType 自动选择对应的表单控件(input/select/date/upload 等)
  • 自动处理表单校验(必填、格式、自定义规则)
  • 支持业务自定义表单项

扩展机制

扩展发生在业务项目侧:框架在 form-item-config.js 中通过 $businessFormItemConfig 合并业务项目的配置(Webpack 将该别名指向业务项目下的 app/pages/widgets/schema-form/form-item-config.js,若不存在则指向空白模块)。业务项目在使用框架时,若需要自定义表单项,在业务项目下新增自定义组件,并在业务项目的 form-item-config 中注册;在 Model 配置中为该字段指定对应 comType 即可使用。

4.3 schema-table:表格自动渲染

schema-table 根据 schema 配置自动渲染表格,支持列配置、分页、操作按钮等。

核心能力

  • 根据 tableOption 自动渲染表格列(列宽、对齐、格式化等)
  • 支持表头操作按钮(由 table-panel 配合 tableConfig.headerButtons 渲染)、行操作按钮(tableConfig.rowButtons)

操作按钮机制

通过 tableConfig 配置操作按钮的事件:

{
  tableConfig: {
    headerButtons: [
      {
        label: '创建用户',
        eventKey: 'showComponent',
        eventOption: { comName: 'createForm' }
      }
    ],
    rowButtons: [
      {
        label: '编辑',
        eventKey: 'showComponent',
        eventOption: { comName: 'editForm' }
      },
      {
        label: '删除',
        eventKey: 'remove',
        eventOption: {
          params: { id: 'schema::id' }  // 从表格行数据中取 id
        }
      }
    ]
  }
}

4.4 schema-search-bar:搜索栏自动渲染

schema-search-bar 根据 schema 配置自动渲染搜索条件。

核心能力

  • 根据 searchOption.comType 自动选择搜索控件类型
  • 支持多种搜索控件(input、select、date-range、cascader 等)
  • 自动处理搜索参数的收集和传递

扩展机制

与 schema-form 类似,业务可以扩展自定义搜索项类型

4.5 动态组件注册机制

Elpis 通过 component-config.js 实现动态组件的注册和使用。

位于 app/pages/dashboard/complex-view/schema-view/components/component-config.js

import CreateForm from './create-form/create-form.vue';
import EditForm from './edit-form/edit-form.vue';
import DetailPanel from './detail-panel/detail-panel.vue';

// 业务扩展 component 配置
import BusinessComponentConfig from '$businessComponentConfig';

const ComponentConfig = {
  createForm: { component: CreateForm },
  editForm: { component: EditForm },
  detailPanel: { component: DetailPanel },
};

export default {
  ...ComponentConfig,
  ...BusinessComponentConfig,
};

使用方式

在 Model 配置的 componentConfig 中声明要使用的组件:

{
  componentConfig: {
    createForm: {
      title: '创建用户',
      saveBtnText: '保存'
    },
    editForm: {
      mainKey: 'id',
      title: '编辑用户',
      saveBtnText: '更新'
    }
  }
}

框架会自动:

  1. 根据 componentConfig 的 key(如 createForm)查找对应的组件
  2. 提取对应的 schema(通过 buildDtoSchema(_schema, 'createForm')
  3. 将 schema 和 config 传递给组件
  4. 在需要时(如点击"创建用户"按钮)动态渲染该组件

扩展机制

与 schema-form 类似,业务可以扩展自定义搜索项类型

4.6 组件库的设计价值

标准化

  • 统一的 Schema 接口,所有组件遵循相同的数据协议
  • 统一的交互模式,降低用户学习成本

灵活性

  • 支持业务自定义表单项、搜索项
  • 支持业务自定义动态组件
  • 支持业务覆盖默认组件行为

可维护性

  • 组件逻辑集中管理,统一升级
  • Schema 配置与组件实现分离,需求变更时只需修改配置

开发效率

  • 一个完整的 CRUD 页面,从编写 Model 配置到页面渲染,无需编写任何组件代码
  • 新增字段、修改展示逻辑,只需调整 Model 配置

五、npm 包抽象封装

第五阶段的核心任务是将 Elpis 框架封装为可发布的 npm 包,实现框架与业务的完全解耦。

5.1 包结构设计

Elpis 作为一个完整的框架,包含了服务端内核、前端工程化、Vue3 组件库等多个模块。

入口与使用

index.js 作为包的统一入口,对外主要暴露两个能力:serverStartfrontendBuild。业务项目安装 @5c24/elpis 后,通过 serverStart(options) 启动服务端(options 透传给 elpis-core),通过 frontendBuild(env) 编译构建前端(env 为 devproduction 分别走开发构建与生产构建)。

5.2 框架与业务的解耦

通过 npm 包封装,实现了框架代码与业务代码的完全分离。

框架职责

  • 提供 Koa 服务端内核
  • 提供 Webpack 工程化能力
  • 提供 Vue3 组件库
  • 提供 Loader 机制
  • 提供配置管理

业务职责

  • 编写 Model 配置
  • 实现业务 Controller/Service
  • 实现自定义组件
  • 配置环境变量
  • 编写业务逻辑

目录结构

业务项目/
├── node_modules/
│   └── @5c24/elpis/        # 框架代码
├── app/
│   ├── controller/          # 业务 controller
│   ├── service/             # 业务 service
│   ├── router/              # 业务路由
│   ├── router-schema/       # 业务路由 schema
│   ├── pages/               # 业务前端页面
│   └── config/              # 业务配置
├── config/
│   ├── config.default.js
│   ├── config.local.js
│   ├── config.beta.js
│   └── config.prod.js
└── package.json

六、未来发展方向

6.1 TypeScript 类型支持

现状

  • 当前框架使用 JavaScript 编写
  • 缺少类型约束,容易出现运行时错误

方向

  • 使用 TypeScript 重构框架核心代码
  • 为 Model 配置提供完整的类型定义
  • 为动态组件提供泛型支持
  • 为业务开发提供类型提示和编译时校验

价值

  • 减少低级错误
  • 提升开发体验
  • 便于大型项目维护

6.2 低代码可视化编辑器

现状

  • Model 配置通过手写 JSON 完成
  • 需要一定的学习成本

方向

  • 开发可视化的 Model 配置编辑器
  • 拖拽式配置字段、按钮、表单项
  • 实时预览配置效果
  • 导出 Model 配置文件

价值

  • 降低使用门槛
  • 非技术人员也能配置页面
  • 提升配置效率

6.3 多端支持

现状

  • 仅支持 PC 端管理后台

方向

  • 移动端响应式适配
  • 小程序版本支持
  • App 版本支持(React Native / Flutter)

价值

  • 扩大框架应用场景

总结

Elpis 框架从 0 到 1 的演进,经历了五个核心阶段:

  1. Node.js 服务端内核引擎:基于 Koa2 实现 Loader 机制,通过目录约定和自动加载,最大程度降低框架与业务的耦合
  2. Webpack5 工程化建设:实现多环境构建、自研热更新、业务扩展能力
  3. Vue3 领域模型架构建设:通过 Model 配置 + DSL 解析引擎实现"80%共性 20%定制化"
  4. Vue3 动态组件库建设:基于 JsonSchema 驱动的组件库,实现"配置即页面"
  5. npm 包抽象封装:实现框架与业务的完全解耦

核心设计理念

  • 框架优先,业务扩展:Loader 机制确保框架能力与业务逻辑的有机结合
  • 配置驱动:通过 Model 配置描述业务模型,框架自动渲染页面
  • 80%共性 20%定制化:标准功能通过配置实现,特殊需求通过扩展机制实现
  • 渐进式增强:业务可以从最小配置开始,逐步扩展功能