Elpis - 基于 Koa + Vue3 的企业级全栈应用框架

17 阅读11分钟

前言

对于大多数中后台开发者而言,日常开发的核心工作往往聚焦于业务逻辑的CRUD实现——无论是简单的基础功能还是复杂的业务场景,本质上都是对数据的增删改查操作,存在大量重复性劳动。 全栈领域模型框架elpis正是为解决这一痛点而生。我们通过模型驱动开发(MDD) 对业务模型进行抽象建模,创新性地实现了:

  1. 标准化描述体系 - 通过统一模型定义前端UI界面、后端API接口及数据库Schema
  2. 自动化生成引擎 - 内置三大核心引擎:
    • 前端UI渲染引擎:动态生成界面
    • 后端接口生成引擎:自动输出RESTful API
    • 数据库建表引擎:智能构建数据存储层
  3. 全链路协同 - 模型变更实时同步至前后端及数据库,确保系统一致性 这种模型驱动的开发范式,将传统CRUD的开发效率提升10倍以上,让开发者能够专注于真正的业务创新而非重复编码。

elpis npm包下载使用:点击这里

1. elpis-core 设计

KOA.jpg

BFF.jpg

1.1 elpis-core 是什么?

elpis是一款面向开发者的企业级全栈框架,其核心目标是通过约定范式彻底解决重复性CRUD开发的效率问题。基于Node.js技术栈(Koa2驱动),elpis以服务化形式运行,为开发者提供了一套开箱即用的高效开发范式。

核心设计理念:约定优于配置

elpis通过严格的约定范式,将开发流程标准化。开发者只需遵循约定编写业务模块,框架即可自动完成功能集成,显著降低重复劳动。

智能加载器体系

elpis内置多类加载器,实现模块的自动化集成:

  • 环境配置:configLoader
  • 功能扩展:extendLoader + middlewareLoader
  • 逻辑分层:serviceLoader + controllerLoader
  • 路由管理:routerSchemaLoader + routerLoader

所有模块将智能注入Koa2的服务上下文(Context)应用实例(App) ,开发者可直接调用,无需关注底层集成细节。

灵活的可扩展性

在保持约定范式的同时,elpis允许以上所有加载器都可以添加自定义扩展

通过这种 "约定为主,扩展为辅" 的设计,elpis既保障了开发效率,又满足了企业级项目的定制需求。

1.2 loader 代码示例

以extendLoader为例,实现代码如下:

const glob = require('glob')
const path = require('path')
const { sep } = path
const { set } = require('lodash')

/**
 * extend loader
 * @param {object} app  Koa 实例
 * 
 * 加载所有 extend,可通过 'app.${目录}.${文件}' 访问
 * 
    例子:
    app/extend
        丨 -- custom-module
                |  -- custom-extend.js
    
    => app.customModule.customExtend
 * 
 */
module.exports = (app) => {
  // 读取 elpis/app/extend/**.js 下所有文件
  const elpisExtendPath = path.resolve(__dirname, `..${sep}..${sep}app${sep}extend`)
  const elpisFileList = glob.sync(path.resolve(elpisExtendPath, `.${sep}**${sep}**.js`))
  elpisFileList.forEach((file) => handleFile(file))

  // 读取 业务根目录/app/extend/**.js 下所有文件
  const bussinessExtendPath = path.resolve(app.bussinessPath, `.${sep}extend`)
  const bussinessFileList = glob.sync(path.resolve(bussinessExtendPath, `.${sep}**${sep}**.js`))
  bussinessFileList.forEach((file) => handleFile(file))

  // 把内容加载到 app 下
  function handleFile(file) {
    // 提取文件名称
    let name = path.resolve(file)

    // 截取路径 app/extend/custom-module/custom-extend.js => custom-module/custom-extend
    name = name.substring(
      name.lastIndexOf(`extend${sep}`) + `extend${sep}`.length,
      name.lastIndexOf(`.`)
    )
    // 把 '-' 统一改成驼峰式,  custom-module/custom-extend => customModule.customExtend
    name = name.replace(/[_-][a-z]/gi, (s) => s.substring(1).toUpperCase()).replace(sep, '.')

    // 过滤app已经存在的key
    for (const key in app) {
      if (key === name) {
        console.log(`[${name}] is already exists in app, please check your extend file.`)
        return
      }
    }

    // 挂载 extend 到 app 上
    set(app, name, require(path.resolve(file))(app))
  }
}

2. elpis 工程化

elpis企业级多系统框架:混合渲染与智能工程化体系

elpis致力于构建新一代企业级应用框架,其核心创新在于混合渲染架构全链路工程化解决方案,完美平衡多系统集成的复杂度与开发体验。

混合渲染架构设计

采用关键页面SSR,非关键页面CSR的复合渲染模式

  • 系统入口层:通过SSR动态分发多站点入口,实现服务端精准路由控制
  • 业务应用层:每个入口承载独立SPA应用,保持前端交互体验一致性
  • 模板引擎:智能生成差异化页面模板,自动注入代码块(如埋点、权限校验等)

全生命周期工程化支持

  1. 生产级构建流水线

    • 多环境适配:自动处理ES语法降级、Polyfill注入
    • 性能优化:代码分包(Code Splitting)、Tree Shaking、Gzip压缩
    • 质量保障:Bundle分析报告、依赖大小可视化
  2. 极致开发体验

    • 热更新(HMR)体系:模块级热替换,保存即生效
    • 增量编译:毫秒级响应代码变更
    • 调试增强:SourceMap与错误追踪深度集成
  3. 标准化交付方案

    • 框架即产品:完备的npm包发布规范
    • 版本管理:语义化版本控制(SemVer)
    • 私有化支持:无缝对接企业私有仓库

框架定位

elpis既是开发加速器(通过约定范式提升效率),又是工程规范实施者(通过标准化流程保障质量),最终实现:

  • 多系统切换成本降低70%
  • 构建效率提升3倍
  • 生产环境稳定性达99.9%

2.1 SSR 入口文件的处理

背景痛点

在构建多入口SSR应用时,传统方案需要为每个入口重复编写框架初始化代码(如Vue的createApp或React的createRoot),导致:

  1. 代码冗余度高
  2. 维护成本增加
  3. 技术栈升级困难

解决方案

我们设计了标准化应用启动器(Bootloader) ,通过抽象框架初始化逻辑实现:

  1. 统一初始化接口

    • 提供 boot.js 核心模块,不同的项目直接调用即可
    • 封装框架特有的实例化过程,获取 Koa 注入模板页面的__ELPIS_ENTRY_NAME__即可动态获取路由基础路径
// boot.js
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/theme-chalk/index.css'
import 'element-plus/theme-chalk/dark/css-vars.css'
import '@elpis/assets/custom.css'
import pinia from '@elpis/store'
import { createRouter, createWebHistory } from 'vue-router'

/**
 * vue 页面主入口,用于启动 vue
 * @param pageComponent vue 入口文件
 * @param routes 路由列表
 * @param libs 页面依赖的第三方包
 * @param basePath 路由基础路径
 */
export default (pageComponent, { routes, libs } = {}) => {
  // 动态获取路由基础路径
  const entryName =
    typeof window !== 'undefined' && window.__ELPIS_ENTRY_NAME__ ? window.__ELPIS_ENTRY_NAME__ : ''
  const basePath = entryName ? `/view/${entryName}` : '/view'

  const app = createApp(pageComponent)
  app.use(ElementPlus)
  app.use(pinia)

  //引入第三方包
  if (libs && libs.length) {
    for (let i = 0; i < libs.length; i++) {
      app.use(libs[i])
    }
  }

  //页面路由
  if (routes && routes.length) {
    const router = createRouter({
      history: createWebHistory(basePath),
      routes
    })
    app.use(router)
    router.isReady().then(() => app.mount('#app'))
  } else {
    app.mount('#app')
  }
}

2.2 entry 入口文件的处理

当前挑战

在传统Webpack配置中,多入口项目通常需要在webpack.base.js中显式声明每个入口的模板和代码块:

// 反模式:手动声明每个入口
entry: {
  entryA: {
    import: './src/entryA.js',
    template: './templates/a.html',
    chunks: ['vendor', 'common', 'entryA']
  },
  entryB: {
    // 重复配置...
  }
  // 随着入口增长,配置急剧膨胀
}

这种模式会导致:

  1. 配置文件臃肿难维护
  2. 新增入口需修改核心配置
  3. 容易产生配置冲突

工程化解决方案

我们采用约定优于配置原则,将入口文件放在pages/**/entry.*.js,通过自动化处理实现:

// 动态构造 elpisPageEntrys  elpisHtmlWebpackPluginList
const elpisPageEntrys = {}
const elpisHtmlWebpackPluginList = []
// 获取 elpis/app/pages 目录下所有入口文件 (entry.xx.js)
const elpisEntryList = path.resolve(__dirname, '../../pages/**/entry.*.js')
glob
  .sync(elpisEntryList)
  .forEach((file) => handleFile(file, elpisPageEntrys, elpisHtmlWebpackPluginList))

// 动态构造 businessPageEntrys  businessHtmlWebpackPluginList
const businessPageEntrys = {}
const businessHtmlWebpackPluginList = []
// 获取 业务根目录/app/pages 目录下所有入口文件 (entry.xx.js)
const businessEntryList = path.resolve(process.cwd(), './app/pages/**/entry.*.js')
glob
  .sync(businessEntryList)
  .forEach((file) => handleFile(file, businessPageEntrys, businessHtmlWebpackPluginList))

// 构造相关 webpack 处理的数据结构
function handleFile(file, entries = {}, htmlWebpackPluginList = []) {
  const entryName = path.basename(file, '.js')
  entries[entryName] = file
  htmlWebpackPluginList.push(
    new HtmlWebpackPlugin({
      // 指定要使用的模板文件
      template: path.resolve(__dirname, '../../view/entry.tpl'),
      // 要注入的代码块
      chunks: [entryName],
      // 产物(最终模板)输出路径
      filename: path.resolve(process.cwd(), 'app/public/dist/', `${entryName}.tpl`),
      inject: true,
      minify: {
        removeComments: true,
        collapseWhitespace: true,
        removeAttributeQuotes: true
      }
    })
  )
}
module.exports ={
  entry: Object.assign({}, elpisPageEntrys, businessPageEntrys),
  plugins: [ ...htmlWebpackPluginList ]
}

2.3 打包优化

性能挑战

随着项目规模增长,传统构建模式会面临两大核心问题:

  1. 构建速度指数级下降
    • 典型表现:200+模块项目冷启动构建超过8分钟
    • 根本原因:未优化的依赖解析和全量编译
  2. 产物体积失控
    • 常见问题:主包超过2MB导致首屏加载缓慢
    • 关键因素:未做代码分割和按需加载

系统性解决方案

一、构建加速体系

  1. 增量编译优化
// webpack配置
cache: {
    type: 'filesystem',
    buildDependencies: { config: \[\_\_filename] }
}
  • 效果:二次构建速度提升70%+
  1. 多进程处理
// 使用thread-loader并行化
npm install thread-loader --save-dev
  1. 依赖预编译
externals: {
    vue: 'Vue',
    lodash: '\_'
}

二、代码瘦身策略

  1. 智能代码分割
{
    optimization: {
      /**
       * 把 js 文件打包成3种类型
       * 1. vendor 第三方lib库,基本不会改动,除非依赖版本升级
       * 2. common 业务组件代码的公共部分收取出来,改动较少
       * 3. entry.{page}: 不同页面 entry 里的业务组件代码的差异部分,会经常改动
       * 目的:把改动和引用频率不一样的 js 区分出来,以达到更好利用浏览器缓存效果
       */
      splitChunks: {
        chunks: 'all', //对同步和异步模块都进行分割
        // maxSize: 500000, // 500KB
        // minSize: 30000, // 30KB
        maxAsyncRequests: 10, // 每次异步加载的最大并行请求数
        maxInitialRequests: 10, // 入口点的最大并行请求数
        cacheGroups: {
          vendor: {
            // 第三方库
            name: 'vendor',
            test: /[\\/]node_modules[\\/]/,
            priority: 20, // 优先级,数字越大,优先级越高
            enforce: true, // 强制执行
            reuseExistingChunk: true // 复用已有的公共 chunk
          },
          common: {
            // 公共模块
            name: 'common',
            test: /[\\/]common|components[\\/]/,
            minChunks: 2, // 被两处引用的即被归为公共模块
            priority: 10,
            reuseExistingChunk: true
          }
        }
      },
      minimize: true,
      // 将 webpack 运行时代码抽离成单独文件
      runtimeChunk: true
    }
 }
  1. 按需加载
// 动态导入语法
const Login = () => import(/* webpackPrefetch: true */ './Login.vue')
  1. 高级压缩
new TerserPlugin({
  parallel: true,
  terserOptions: { compress: { drop_console: true } }
})

2.4 开发环境搭建

核心架构设计

我们采用 Express + Webpack 中间件 构建高性能开发服务器,实现真正的模块热替换(HMR)能力:

// webpack.dev.js
// 基类配置
const path = require('path')
const merge = require('webpack-merge')
const os = require('os')
const webpack = require('webpack')

//基础配置
const baseConfig = require('./webpack.base.js')

//devServer配置
const DEV_SERVER_CONFIG = {
  HOST: '127.0.0.1',
  PORT: 9002,
  HMR_PATH: '__webpack_hmr',
  TIMEOUT: 20000
}

//开发阶段的 entry 配置需要加入的hmr
Object.keys(baseConfig.entry).forEach((entryName) => {
  if (entryName !== 'vendor') {
    baseConfig.entry[entryName] = [
      //主入口
      baseConfig.entry[entryName],
      //hmr更新入口
      `${require.resolve('webpack-hot-middleware/client')}?path=http://${DEV_SERVER_CONFIG.HOST}:${DEV_SERVER_CONFIG.PORT}/${DEV_SERVER_CONFIG.HMR_PATH}&timeout=${DEV_SERVER_CONFIG.TIMEOUT}&reload=true`
    ]
  }
})

// 生产环境配置
const webpackConfig = merge.smart(baseConfig, {
  mode: 'development',
  //source-map 开发工具,呈现代码的映射关系,便于在开发过程中调试代码
  devtool: 'eval-cheap-module-source-map',
  output: {
    filename: 'js/[name]_[chunkhash:8].bundle.js',
    path: path.resolve(process.cwd(), './app/public/dist/dev/'), // 输出路径
    publicPath: `http://${DEV_SERVER_CONFIG.HOST}:${DEV_SERVER_CONFIG.PORT}/public/dist/dev/`, // 静态资源访问路径(在devServer内存中)
    globalObject: 'this'
  },
  module: {
      //...此处省略
  },
  plugins: [
    //  HMR热更新
    new webpack.HotModuleReplacementPlugin({
      multiStep: false
    })
  ]
})

module.exports = { webpackConfig, DEV_SERVER_CONFIG }

本地开发环境启动入口文件,如下:

// dev.js
const webpack = require('webpack')
const express = require('express')
const consoler = require('consoler')
const path = require('path')
const devMiddleware = require('webpack-dev-middleware')
const hotMiddleware = require('webpack-hot-middleware')

module.exports = () => {
  const { webpackConfig, DEV_SERVER_CONFIG } = require('./config/webpack.dev.js')

  const app = express()

  const compiler = webpack(webpackConfig)

  //指定静态文件目录
  app.use(express.static(path.join(process.cwd(), './app/public/dist')))

  //引用 devMiddleware 中间件(监控文件改动)
  app.use(
    devMiddleware(compiler, {
      //落地文件
      writeToDisk: (filePath) => filePath.endsWith('.tpl'), // 页面模板(如 entry.page1.tpl)需要实际写入磁盘,通过 express.static 提供访问。
      //资源路径
      publicPath: webpackConfig.output.publicPath, // JS/CSS 等资源由 webpack-dev-middleware 托管在内存中,走 HMR 热更新。
      //headers配置
      headers: {
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
        'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization'
      },
      stats: {
        colors: true
      }
    })
  )
  //引用 hotMiddleware 中间件(实现HMR)
  app.use(
    hotMiddleware(compiler, {
      path: `/${DEV_SERVER_CONFIG.HMR_PATH}`,
      log: false
    })
  )

  consoler.info('请等待webpack初次构建完成提示......')

  //启动 devServer
  const port = DEV_SERVER_CONFIG.PORT
  app.listen(port, () => {
    console.log(`webpack-dev-server is listening on port ${port}`)
  })
}

2.5 增加启动脚本

{
  "build:dev": "cross-env NODE_ENV=local node --max_old_space_size=4096 ./app/webpack/dev.js",
}

3. 领域模型 DSL 设计

DSL领域模型.jpg DSL(Domain Specific Language,领域特定语言 )领域模型是为特定业务领域定制的语言及配套模型体系,聚焦解决该领域的问题。

3.1 为什么用 DSL 领域模型?

1. 降低领域沟通成本

  • 让业务人员(非技术)也能 “用领域语言” 参与开发:比如运营可通过配置 DSL 调整页面表格列,不用懂 Vue/React代码。
  • 统一 “业务描述 - 技术实现” 的语言:开发说 “用 schema-table 组件”,业务能理解是 “表格展示”,减少需求传递偏差。

2. 提升开发效率(复用 + 配置化 = 80% / 自定义 + 扩展 = 20%)

  • 组件 / 模板复用:沉淀通用 DSL 模型(如 dashboard 模板),新需求直接改配置,不用重复写页面逻辑。
  • 适配后端变化:后端接口字段变了,只需在 DSL 模型层(中间层)做数据转换,前端组件不用逐个修改

3. 聚焦领域问题解决

  • 相比通用代码,DSL 更 “轻量、专注”:比如用 DSL 描述 “页面要一个带筛选的表格”,只需几行配置;用通用代码则要写组件引入、数据请求、渲染逻辑,冗余且易出错。

3.2 如何在项目中使用?

一个领域模型可以衍生出若干个项目,领域模型项目的关系是对象继承关系,项目(子类)继承于领域模型(基类),领域模型可以沉淀各个项目中重复功能/页面,实现复用。

通过如下配置,即可生成一个项目

{
  mode: 'dashboard', // 模板类型,不同模板类型对应不一样的模板数据结构
  name: '', //名称
  desc: '', //描述
  icon: '', //图标
  homePage: '', //首页(项目配置)
  menu: [
    {
      key: '', //菜单唯一描述
      name: '', //菜单名称
      menuType: '', //枚举值  group/module

      // 当menuType == group 时,可填
      subMenu: [{}],

      moduleType: '', // 枚举值  sider/iframe/custom/schema

      //当 moduleType == sider 时
      siderConfig: {
        menu: [{}]
      },

      //当 moduleType == iframe 时
      iframeConfig: {
        path: '' // iframe 路径
      },

      //当 moduleType == custom 时
      customConfig: {
        path: '' // 自定义路由路径
      },

      //当 moduleType == schema 时
      schemaConfig: {
        api: '', // 数据源API (遵循 restfull 规范)
        schema: {
          type: 'object',
          properties: {
            key: {
              type: '', //字段类型
              label: '', //字段中文名
              // 字段在 table 中的相关配置
              tableOption: {
                ...elTableColumnConfig, // 标准 el-table-column 配置
                toFixed: 0,
                visible: true // 默认为 true (false表示不在表单中显示)
              },
              // 字段在 search-bar 中的相关配置
              searchOption: {
                ...elComponentConfig, // 标准 el-component-config 配置
                comType: '', // 配置组件类型  input/select/...
                default: '', // 默认值

                // 当 comType === 'select' 时,可配置
                enumList: [
                  {
                    label: '',
                    value: ''
                  }
                ],

                // 当 comType === 'dynamicSelect' 时,可配置
                api: '' // 数据源API (遵循 restfull 规范)
              },
              // 字段在不同动态 component 中的相关配置,前缀对应 componentConfig 中的键值
              // 如:componentConfig.createForm  这里对应 createFormOption
              // 字段在 createForm 中相关配置
              createFormOption: {
                ...elComponentConfig, // 标准 el-component-config 配置
                comType: '', // 配置组件类型  input/select/...
                visible: true, // 是否展示,默认为 true
                disabled: false, // 是否禁用,默认为 false
                default: '', // 默认值

                // 当 comType === 'select' 时,可配置
                enumList: [
                  {
                    label: '',
                    value: ''
                  }
                ]
              },
              // 字段在 editForm 中相关配置
              editFormOption: {
                ...elComponentConfig, // 标准 el-component-config 配置
                comType: '', // 配置组件类型  input/select/...
                visible: true, // 是否展示,默认为 true
                disabled: false, // 是否禁用,默认为 false
                default: '', // 默认值

                // 当 comType === 'select' 时,可配置
                enumList: [
                  {
                    label: '',
                    value: ''
                  }
                ]
              },
              detailPanelOption: {
                ...elComponentConfig // 标准 el-component-config 配置
              },
              apiOption: {}, // 数据源配置
              dbOption: {} // 数据库配置
            }
          },
          required: [] // 标记哪些字段为必填项
        },
        tableConfig: {
          headerButtons: [
            {
              label: '', // 按钮中文名
              eventKey: '', // 按钮事件名
              // 按钮具体配置
              eventOption: {
                // 当 eventKey === 'showComponent'
                comName: '' // 组件名
              },
              ...elButtonConfig // 标准 el-button 配置
            }
          ], // 表头按钮
          rowButtons: [
            {
              label: '', // 按钮中文名
              eventKey: '', // 按钮事件名
              eventOption: {
                // 当 eventKey === 'showComponent'
                comName: '', // 组件名

                // 当 eventKey === 'remove'
                params: {
                  idKey: 'schema::idKey' // 当格式为 schema::tableKey 的时候,到 table 中找相应的字段
                }
              }, // 按钮具体配置
              ...elButtonConfig // 标准 el-button 配置
            }
          ] // 行按钮
        }, // table 相关配置
        searchConfig: {}, //search-bar 相关配置
        // 动态组件 相关配置
        componentConfig: {
          // createForm 表单相关配置
          createForm: {
            title: '', // 表单标题
            saveBtnText: '' // 保存按钮文案
          },
          // editForm 表单相关配置
          editForm: {
            mainKey: '', // 表单主键,用于唯一标识要修改的数据对象
            title: '', // 表单标题
            saveBtnText: '' // 保存按钮文案
          },
          detailPanel: {
            mainKey: '', // 表单主键,用于唯一标识要修改的数据对象
            title: '' // 表单标题
          }
        }
      }
    }
  ]
}

SchemaView 页面的实现是项目的核心,是在菜单 menu 中配置一份基于 json-schema 规范的 schema配置,结合各个解析器,实现页面渲染 schema.jpg

3.3 如何通过schema配置生成各个解析器?

// 通用构建 schema 方法 (清除噪音)
  const buildDtoSchema = (_schema, comName) => {
    if (!_schema?.properties) return {}
    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 的部分,存放到 dtoProps 中
        for (const pKey in props) {
          if (pKey.indexOf('Option') < 0) {
            dtoProps[pKey] = props[pKey]
          }
        }
        // 处理 comName Option
        dtoProps = Object.assign({}, dtoProps, { option: props[`${comName}Option`] })

        // 处理 required 字段
        const { required } = _schema
        if (required && Array.isArray(required) && required.includes(key)) {
          if (dtoProps.option) {
            dtoProps.option.required = true
          }
        }

        dtoSchema.properties[key] = dtoProps
      }
    }
    return dtoSchema
  }

  // 构造 schemaConfig 相关配置,输送给 schemaView 解析
  const buildData = () => {
    const { key, sider_key: siderKey } = route.query
    const menuItem = menuStore.findMenuItem({
      key: 'key',
      value: siderKey ?? key
    })
    if (menuItem && menuItem.schemaConfig) {
      const { schemaConfig: sConfig } = menuItem

      const configSchema = JSON.parse(JSON.stringify(sConfig.schema))
      api.value = sConfig.api ?? ''
      tableSchema.value = {}
      tableConfig.value = undefined
      searchSchema.value = {}
      searchConfig.value = undefined
      components.value = {}
      nextTick(() => {
        // 构造 tableSchema 和 tableConfig
        tableSchema.value = buildDtoSchema(configSchema, 'table')
        tableConfig.value = sConfig.tableConfig ?? {}

        // 构造 searchSchema 和 searchConfig
        const dtoSearchSchema = buildDtoSchema(configSchema, 'search')
        for (const key in dtoSearchSchema.properties) {
          if (route.query[key] !== undefined) {
            dtoSearchSchema.properties[key].option.default = route.query[key]
          }
        }
        searchSchema.value = dtoSearchSchema
        searchConfig.value = sConfig.searchConfig ?? {}

        // 构造 components = { comKey: { schema: {}, config: {} } }
        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
        }
      })
    }
  }

生成过后的schema结构如下所示:

/**
   * schemaForm 的 schema 配置,结构如下
     {
          type: 'object',
          properties: {
            key: {
              ...schema, // 标准 schema 配置
              type: '', // 字段类型
              label: '', // 字段中文名
              // 字段在 form 中的相关配置
              option: {
                ...elComponentConfig, // 标准 el-component-config 配置
                comType: '', // 配置组件类型  input/select/...
                visible: true, // 是否展示,默认为 true
                disabled: false, // 是否禁用,默认为 false
                default: '', // 默认值
                required: false // 是否必填,默认为 false

                // 当 comType === 'select' 时,可配置
                enumList: [
                  {
                    label: '',
                    value: ''
                  }
                ]
              }
            }
          },
        }
     */

/**
   * schemaSearchBar 的 schema 配置,结构如下
    {
        type: 'object',
        properties:  {
            key: {
                ...schema, // 标准 schema 配置
                type: '', //字段类型
                label: '', //字段中文名
                // 字段在 search-bar 中的相关配置
                option: {
                    ...elComponentConfig, // 标准 el-component-config 配置
                    comType: '', // 配置组件类型  input/select/...
                    default: '' // 默认值
                }
            }
        }
    }
   */

/**
   * schemaTable 的 schema配置,结构如下:
    {
      type: 'object',
      properties: {
        key: {
          ...schema, // 标准 schema 配置
          type: '', //字段类型
          label: '', //字段中文名
          // 字段在 table 中的相关配置
          option: {
            ...elTableColumnConfig, // 标准 el-table-column 配置
            visible: true // 默认为 true (false 或 不配置,表示不再表单中显示)
          }
        }
      }
    }
   */

有了这样的设计,我们只需维护一份 schema 配置,即可直接生成一个CRUD的查询页,极大地提高了开发效率。

4. 发布npm包

纯净内核架构

  • 框架作为技术中台,严格遵循"零业务逻辑"准则

    入口文件提供服务端基础,以及frontendBuild、serverStart,提供给业务项目调用。

// 引入 elpis-core
const ElpisCore = require('./elpis-core')
// 引入 前端工程化构建方法
const FEBuildDev = require('./app/webpack/dev.js')
const FEBuildProd = require('./app/webpack/prod.js')

module.exports = {
  /**
   * 服务端基础
   */
  Controller: { Base: require('./app/controller/base.js') },
  Service: { Base: require('./app/service/base.js') },

  /**
   * 编译构建前端工程
   * @params  env 环境变量 local/production
   */
  frontendBuild(env) {
    if (env === 'local') {
      FEBuildDev()
    } else if (env === 'production') {
      FEBuildProd()
    }
  },

  /**
   * 启动 Elpis
   * @params  options 项目配置,透传到 elpis-core
   */
  serverStart(options = {}) {
    const app = ElpisCore.start(options)
    return app
  }
}

通过如下 webpack 配置整合 elpis 及 业务代码,配置 alias 暴露给业务项目调用 elpis 内部功能。

{
  alias: (() => {
    const aliasMap = {}
    const blankModulePath = path.resolve(__dirname, '../libs/blank.js') // 空文件兜底

    // dashboard 路由拓展配置
    const bussinessDashboardRouterConfig = path.resolve(
      process.cwd(),
      './app/pages/dashboard/router.js'
    )
    aliasMap['@bussinessDashboardRouterConfig'] = fs.existsSync(bussinessDashboardRouterConfig)
      ? bussinessDashboardRouterConfig
      : blankModulePath

    // schemaView component 扩展配置
    const bussinessComponentConfig = path.resolve(
      process.cwd(),
      './app/pages/dashboard/components/schemaView/component-config.js'
    )
    aliasMap['@bussinessComponentConfig'] = fs.existsSync(bussinessComponentConfig)
      ? bussinessComponentConfig
      : blankModulePath

    // schemaForm form-item 扩展配置
    const bussinessFormItemConfig = path.resolve(
      process.cwd(),
      './app/pages/components/schemaForm/form-item-config.js'
    )
    aliasMap['@bussinessFormItemConfig'] = fs.existsSync(bussinessFormItemConfig)
      ? bussinessFormItemConfig
      : blankModulePath

    // schemaSearchBar search-item 扩展配置
    const bussinessSearchItemConfig = path.resolve(
      process.cwd(),
      './app/pages/components/schemaSearchBar/search-item-config.js'
    )
    aliasMap['@bussinessSearchItemConfig'] = fs.existsSync(bussinessSearchItemConfig)
      ? bussinessSearchItemConfig
      : blankModulePath

    return {
      '@elpis/pages': path.resolve(__dirname, '../../pages'),
      '@elpis/assets': path.resolve(__dirname, '../../pages/assets'),
      '@elpis/common': path.resolve(__dirname, '../../pages/common'),
      '@elpis/curl': path.resolve(__dirname, '../../pages/common/curl.js'),
      '@elpis/utils': path.resolve(__dirname, '../../pages/common/utils.js'),

      '@elpis/components': path.resolve(__dirname, '../../pages/components'),
      '@elpis/headerContainer': path.resolve(
        __dirname,
        '../../pages/components/headerContainer/index.vue'
      ),
      '@elpis/siderContainer': path.resolve(
        __dirname,
        '../../pages/components/siderContainer/index.vue'
      ),
      '@elpis/schemaSearchBar': path.resolve(
        __dirname,
        '../../pages/components/schemaSearchBar/index.vue'
      ),
      '@elpis/schemaForm': path.resolve(
        __dirname,
        '../../pages/components/schemaForm/index.vue'
      ),
      '@elpis/schemaTable': path.resolve(
        __dirname,
        '../../pages/components/schemaTable/index.vue'
      ),

      '@elpis/store': path.resolve(__dirname, '../../pages/store'),
      '@elpis/boot': path.resolve(__dirname, '../../pages/boot.js'),
      ...aliasMap
    }
  })()
}

欢迎使用 elpis npm包:点击这里