@tushi11/elpis
v1.0.34
Published
- Elpis 是一个基于 Node.js + Koa2 + Vue3 的全栈开发平台。旨在通过配置化的方式快速构建企业级管理系统。项目采用模块化架构设计,支持多项目管理。 - elpis 的设计初衷是为了解决重复性的增删改查工作,将常规的增删改查沉淀到模型中,通过模型解析器解析模型配置,加上模型数据渲染组件将数据渲染到页面,并且支持常规交互 - 最终打造一个全栈,全流程去开发一个支持多网站建设的系统平台(应用框架)。能通过配置化去沉淀 80%的重复性需求,并且提供各种各样的定制化能力,可灵活支持剩余
Readme
elpis 企业级应用框架
- Elpis 是一个基于 Node.js + Koa2 + Vue3 的全栈开发平台。旨在通过配置化的方式快速构建企业级管理系统。项目采用模块化架构设计,支持多项目管理。
- elpis 的设计初衷是为了解决重复性的增删改查工作,将常规的增删改查沉淀到模型中,通过模型解析器解析模型配置,加上模型数据渲染组件将数据渲染到页面,并且支持常规交互
- 最终打造一个全栈,全流程去开发一个支持多网站建设的系统平台(应用框架)。能通过配置化去沉淀 80%的重复性需求,并且提供各种各样的定制化能力,可灵活支持剩余 20%的定制化需求开发。
问题
传统开发中,80%的时间消耗在重复性业务逻辑上:
- 基础 CRUD 接口的重复开发
- 表单 / 表格 / 搜索框等组件的重复编写
- 验证逻辑、交互逻辑的重复实现
解决方案
Elpis 通过 DSL 领域模型的开发范式重构开发流程:
统一建模层
- 模型使用继承的理念,一个领域模型可以衍生出若干个项目
智能解析引擎
模型 → 页面:自动生成 CRUD 标准页面
模型 → 组件:动态渲染表单/表格/搜索框/详情面板/...
模型 → 菜单:自动化配置前端路由
- 一个领域模型可以衍生出若干个项目,领域模型和项目的关系是对象继承关系,项目(子类)继承于领域模型(基类),领域模型可以沉淀各个项目中重复的功能/页面,实现复用。
项目技术栈
| 后端技术栈 | 前端技术栈 | | ------------------------------- | --------------------------------- | | 框架: Koa2 | 框架: Vue 3 (Composition API) | | 模板引擎: Nunjucks | UI 组件库: Element Plus | | 测试框架: Mocha + Supertest | 状态管理: Vuex 4 | | 工具库: Lodash, MD5, Glob | 路由: Vue Router 4 | | | 构建工具: Webpack 5 | | | 样式预处理: Less |
项目架构设计
展示层:Vue3 + element-plus + webpack5
- 融合了多页面 SSR-MPA和单页面 CSR-SPA两种模式。在配置多个单页面入口的基础上,赋予每个单页面通过前端路由跳转子页面的能力。
- 通过服务端渲染生成多页面(SSR)入口,里面渲染的不同 页面/路由 都是一个独立的单页面 CSR-SPA 应用,是通过前端路由 router实现子页面间的无缝切换(即首屏是服务端渲染,次屏是客户端渲染的)
- 整个方案利用起首屏里 SEO 的好处,次屏 SPA 的好处
BFF 层:Node.js18(Koa2)
- 细分为接入层、业务层和服务层。
- 接入层主要负责路由分发,通过分析页面请求来判断是进行渲染操作还是调用 BFF 接口;
- 业务层聚焦于前端页面所需的各类业务处理,为页面功能实现提供业务逻辑支撑;
- 服务层则主要承担获取数据的处理工作,保障数据的有效获取与传递。
- 细分为接入层、业务层和服务层。
数据层:MySQL + log4js 日志
- 包括了数据库,日志,后端服务接口以及其他各类数据来源。
(这个系统用的是 nodejs 来作为运行时页面服务器,同时它也是 API 的 BFF 层)
model 配置(核心)
model 的配置是使用 elpis 的重中之重,根据下面给到的 model 示例配置,进行项目 model 模型配置。
Model 是 Elpis 的核心配置单元,通过定义领域模型的结构、交互和接口,驱动全栈功能生成。
{
mode: 'dashboard', // 模板类型,不同模板类型对应不一样的模板数据结构
name: '', // 模板名称
desc: '', // 模板描述
icon: '', // 模板图标
homePage: '/schema?proj_key=pdd&key=product', // 模板首页(项目配置) // proj_key=项目的文件名 || key=头部菜单栏的默认选中项 || sider_key=左侧菜单栏的默认选中项
// 头部菜单
menu: [
{
key: '', // 菜单唯一描述
name: '', // 菜单名称
menuType: '', // 菜单类型(菜单目录、菜单项) group | module
// 当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: {
key1: {
...schema, // 标准 schema 配置(如:minLength: 2,pattern: '^[a-z0-9_]+$')
type: '', // 字段类型(array|string|nubmer|object) // 多个类型传递数组['string','number']
label: '', // 字段的中文名
// 字段在 table 中的相关配置
tableOption: {
...elTableColumnConfig, // 标准 el-table-column 配置
visible: true, // 默认为 true(为 false 时,表示不在 table 中显示)
// 当前字段的格式化配置
formatConfig:{
color: 'red', // 单元格颜色(支持十六进制/rgb/颜色名)
// 1. 自定义格式化函数(优先级最高)// value为当前单元格的值,row为当前行的数据
formatFn: (value, row) => `${value}元`,
// 以下为预设格式化类型配置。(可选,'date'/'enum'/'number') // formatType 和 formatOptions 需同时配置。
// 1. 日期格式化
formatType: 'date', // 预设日期类型
formatOptions: 'YYYY-MM-DD HH:mm:ss', // 日期格式(可选,默认YYYY-MM-DD HH:mm:ss)
// 2. 枚举格式化(如性别、状态)
formatType: 'enum', // 预设枚举类型
formatOptions: { 1: '男', 2: '女', 3: '未知' },
enumColor: { 1: 'green', 2: '#e6a23c', 3: 'orange' } // 枚举颜色映射(支持颜色名/十六进制/rgb)
// 3. 数字格式化(如金额、百分比)
formatType: 'number', // 预设数字类型
formatOptions: { toFixed: 2, unit: '元' }, // 保留2位小数+添加单位
}
},
// 字段在 search-bar 中的相关配置
searchOption: {
...elComponentConfig, // 标准 el-component-column 配置
comType: '', // 配置组件类型(如 input/select/...)
default: '', // 默认值
// 当 comType === select 时
enumList: [], // 下拉框可选项 // 当 comType === 'dynamicSelect' 时,会和 API 可选项进行合并
// 当 comType === 'dynamicSelect'
api: '',
},
// 字段在不同动态 component 中的相关配置,前缀对应 componentConfig 中的键值
// 如:componentConfig.createForm,这里对应 createFormOption
// 字段在 createForm 中相关配置
createFormOption: {
...elComponentConfig, // 标准 el-component 配置
comType: '', // 控件类型(有:input/select/checkbox/input-number/switch/radio/dateTimePicker/textarea/cascader)
visible: true, // 是否展示(true/false),默认为 true
disabled: false, // 是否禁用(true/false),默认为 false
default: '1', // 默认值 (当 comType === 'checkbox' 可配置字符串或数组)
// 当 comType === 'select'|'radio'|'checkbox' 时生效 // 枚举列表
enumList: [
{ value: 1, label: 'option1' },
{ value: 2, label: 'option2' },
],
// 当 comType === 'select'时并使用了api时生效。适配接口返回字段名不一致的情况(如接口返回 name/code,可通过配置映射)
labelKey:'name',
valueKey: 'code',
// 当 comType === 'select'|radio'|'checkbox' 时生效
api: '',
apiParams: '', // 传递接口参数
// 当 comType === 'switch' 时生效
switchModelType:'', // switch 类型,可选值:'boolean'、'string',默认:'boolean'
// 当 comType === 'dateTimePicker' 时。配置 shortcuts 快捷选项时,value 需直接传递 Date 对象。禁止直接使用函数(() => {})作为 value,否则会导致组件无法解析赋值
shortcuts: [
{ text: '今天', value: new Date() },
{
text: '昨天',
// value 不能是函数类型。必须使用立即执行函数提前计算并返回Date对象
value: (() => {
return new Date().setDate(new Date().getDate() - 1);
})(),
},
],
},
// 字段在 editForm 表单中的相关配置
editFormOption: {
...elComponentConfig, // 标准 el-component 配置
comType: '', // 控件型(如 input/select/...)
// ...... 参考 createFormOption
},
detailPanelOption: {
...elComponentConfig, // 标准 el-component 配置
},
},
},
required: [], // 必填字段列表
},
// table 相关配置
tableConfig: {
// 表格头部按钮
headerButtons: [
{
label: '', // 按钮名称
eventKey: '', // 按钮事件名称
// 按钮事件具体配置
eventOption: {
// 当 eventKey === 'showComponent'时,启用 comName,决定调用哪个组件
comName: '', // 组件名称
},
...elButtonConfig, // 标准 el-button 配置
},
// ...
],
// 表格行内按钮
rowButtons: [
// 按钮事件具体配置
{
label: '', // 按钮名称
eventKey: '', // 按钮事件名称
eventOption: {
// 当 eventKey === 'showComponent'时,启用 comName,决定调用哪个组件
comName: '', // 组件名称
// 当 eventKey === 'useApi'
apiOption: {
api: '', // 接口地址
},
// 当 eventKey === 'remove'时,可填
params: {
// paramsKey 等于 参数的键值
// rowValueKey 等于 动态的参数值,格式为 schema::tableKey,到 table 行数据中找相应的字段的值
paramsKey: rowValueKey, // 【 例如 product_id: 'schema::product_id' 】
},
},
...elButtonConfig, // 标准 el-button 配置
},
// 表格多选功能配置
showSelection: 'false' // 默认不显示多选列
// Table所有原生属性(Element Plus TableProps类型)
tableProps: {
rowKey: 'id', // 表格的唯一标识字段。默认是id
......,
},
// 分页所有原生属性(Element Plus PaginationProps类型)
paginationProps: {...}
// ...
],
},
// search-bar 相关配置
searchConfig: {},
// 动态组件 相关配置
componentConfig: {
// create-form 表单相关配置
createForm: {
title: '', // 表单标题
saveBtnText: '', // 保存按钮文案
type: '', // 弹窗样式,可选值:'drawer'、'dialog',默认:'dialog'
dialogWidth: '800px', // 弹窗宽度,默认:'500px'
formLabelWidth: '70px', // 表单项标签宽度,默认:'70px'
},
// edit-form 表单相关配置
editForm: {
mainKey: '', // 表单主键,用于唯一标识要修改的数据对象
title: '', // 表单标题
saveBtnText: '', // 保存按钮文案
type: '', // 弹窗样式,可选值:'drawer'、'dialog',默认:'dialog'
dialogWidth: '800px', // 弹窗宽度,默认:'500px'
formLabelWidth: '70px', // 表单项标签宽度,默认:'70px'
},
// detail-panel 相关配置
detailPanel: {
mainKey: '', // 表单主键,用于唯一标识要修改的数据对象
title: '', // 详情面板标题
type: '', // 弹窗样式,可选值:'drawer'、'dialog',默认:'dialog'
dialogWidth: '800px', // 弹窗宽度,默认:'500px'
},
// ...支持用户动态扩展
},
},
},
],
}node 服务端启动
启动服务端
引入 elpis,并启动。其中,startServer 可以传入配置项 options,它会挂载在 koa-app 上,通过app.options访问。除此之外,options 还会传递给客户端,因此,你可以传递服务端获取到的数据,以实现数据在服务端渲染时服务端和客户端之间的同步。
// 使用框架内置服务
const { serverStart } = require('@tushi11/elpis');
// 启动 elpis 服务
const app = serverStart({
name: 'ElpisDemo',
// 其他配置项
icon: '/static/logo.png',
homePage: '/view/project-list',
});自定义(拓展)服务端
Elpis 支持基于 elpis-core 的自定义服务端开发,目录结构遵循约定:
└── app
├── router-schema # 接口参数校验规则(JSON Schema)
├── router # 路由定义
├── controller # 接口逻辑
├── service # 业务服务
├── extend # 扩展能力(如日志、工具)
└── config # 环境配置(多环境支持)router-schema 是用来定义接口入参出参的 json-schema。实现接口的入参出参校验能力,是 elpis 的中间件
api-params-verify。// app/router-schema/**.js (一定是放在一级目录下的,每份都是配置) module.exports = { '/api/proj/product/list': { get: { query: { type: 'object', properties: { page: { type: 'string', }, size: { type: 'string', }, }, required: ['page', 'size'], }, }, }, };router 定义后端接口路由
module.exports = (app, router) => { const { demo: demoController } = app.controller; router.get('/api/demo', demoController.get.bind(demoController)); };controller 定义接口处理器(面向业务 )
- app 可以通过这样的方式去访问 ==>
app.controller.customModule.customController
module.exports = (app) => { const BaseController = require('@tushi11/elpis').Controller.Base(app); return class customController extends BaseController { async create(ctx) { this.success(ctx, { user_id: 11 }); } }; };- app 可以通过这样的方式去访问 ==>
service 定义接口处理服务(增删查改数据库)
module.exports = (app) => { return class UserService extends BaseService { async get(userId) { const result = await database('t_user').select('*').where({ user_id: userId, status: app.status.NORMAL, }); return result[0] ?? {}; } }; };extend 定义其他扩展能力。
module.exports = (app) => { return require('moment'); };config 定义项目的变量(app/config/config.xxxx.js)
/* * 默认配置 config/config.default.js * 本地配置 config/config.local.js * 测试配置 config/config.beta.js * 生产配置 config/config.prod.js */ // config.prod.js module.exports = { name: 'demo-prod', };
前端开发
构建与启动
// build.js
const { frontendBuild } = require('@tushi11/elpis');
// 编译构建前端工程 - 这里会根据环境变量自动选择启动开发环境还是打包生产环境
frontendBuild(process.env._ENV);多页面拓展(自定义页面)
**约定:**在
app/pages/目录下创建多页面入口( 以entry.xxx.js命名)// app/pages/demo/entry.demo.js import elpisBoot from '@elpisBoot'; // 调用 elpisBoot 中的方法创建vue示例 import DemoVue from './demo.vue'; // 自定义 vue 组件 // 当自定义的 vue组件 使用了<router-view> 可传递页面路由 const routes = [ { path: '/view/auth/login', component: () => import('./complex-view/login/login.vue'), }, ]; /** * elpisBoot 是一个 Vue3 应用的通用启动器函数,用于Vue 实例初始化,实现独立的SPA应用。支持通过配置项实现不同页面的个性化需求。 * vue 页面主入口,用于启动 vue * @params pageComponent vue 入口组件 * @param {Object} options 配置项 * @param {Array} options.routes 路由列表 * @param {Array} options.libs 页面依赖的第三方包 */ elpisBoot(DemoVue, { routes }); // 创建vue示例在
app/pages/定义 vue 页面// demo.vue <template> <div style="color: red;"> demo <el-input v-model="value" /> </div> <router-view></router-view> </template> <script setup> import { ref, watch } from 'vue' const value = ref(''); watch(value, (newvalue, oldvalue) => { console.log(oldvalue, newvalue); }) </script> <style scoped lang="scss"></style>
dashboard 页面扩展
dashboard / custom-view 自定义页面扩展
在
app/pages/dashboard/xxx下写页面// app/pages/dashboard/todo/todo.vue <template> <h1>待开发</h1> </template> <script setup></script> <style lang="scss" scoped></style> <script setup> import { ref, watch } from 'vue' const value = ref(''); watch(value, (newvalue, oldvalue) => { console.log(oldvalue, newvalue); }) </script> <style scoped lang="scss"></style>
dashboard 路由配置
- 在
app/pages/dashboard/router.js中进行配置
module.exports = ({ routes, siderRoutes }) => {
// 头部路由
routes.push({
path: '/view/dashboard/todo',
component: () => import('./todo/todo.vue'),
});
// 侧边路由
siderRoutes.push({
path: 'todo',
component: () => import('./todo/todo.vue'),
});
};组件拓展
扩展位置约定
| 扩展控制 | 目录位置 | 配置文件 |
| :----------------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------ |
| dashboard / schema-view / components | app/pages/dashboard/complex-view/schema-view/components | app/pages/dashboard/complex-view/schema-view/components/components-config.js |
| schema-form | app/pages/widgets/schema-form/complex-view | app/pages/widgets/schema-form/form-item-config.js |
| schema-search-bar | app/pages/widgets/schema-search-bar/complex-view | app/pages/widgets/schema-search-bar/search-item-config.js |
| header-container | app/pages/widgets/header-container/complex-view | app/pages/widgets/header-container/header-config.js |
| | | |
// components-config.js 示例:
import createForm from './create-form/create-form.vue';
const ComponentConfig = {
createForm
};
export default ComponentConfig;
// form-item-config.js 示例:
import textarea from './complex-view/textarea/textarea.vue';
const FormItemConfig = {
textarea,
};
export default FormItemConfig;- schemaView 页面的实现是项目的核心,是在菜单 menu 中配置一份基于 json-schema 规范的 schema 配置,结合各个解析器,实现页面渲染
配置文件拓展
在
app/pages/configs/configs.js进行前端内容配置module.exports = { // 主题 / 布局设置 themeSetting: { menuMode: 'horizontal', // 菜单类型,可选值:'horizontal'、'vertical',默认:'horizontal' dialogMode: 'dialog', // 弹窗样式,可选值:'drawer'、'dialog',默认:'dialog' useI18n: false, // 是否使用国际化 /**1. useI18n 为true 时,全局翻译翻译: <div>{{ t('user.name') }}</div> *2. 带 t:: 前缀,强制翻译(即使全局 useI18n 为 false):<div>{{ t('t::user.name') }}</div> */ // 主题设置组件的默认配置 language: 'zh-cn', showCrumb: true, showProject: true, showLogo: true, isUniqueOpened: false, sideWidth: 200, sideTheme: 'light', sideDarkColor: '#1d2124', openMultipleTabs: true, theme: '#4A5DFF', successTheme: '#67c23a', warningTheme: '#e6a23c', dangerTheme: '#f56c6c', errorTheme: '#f56c6c', infoTheme: '#909399', //信息主题色 }, sysSetting: { terminal: 1, title: '后台管理系统', version: '1.0.0', // 携带项目版本号(便于后端兼容处理) }, // 请求配置 requestSetting: { // 只有一个代理则使用proxy: timeout: 10 * 1000, // 请求超时时长 // baseURL: 'https://java-admin.likeadmin.cn', // proxyIndex: 0, // 默认不配置。配置后,根据下标获取proxyOption配置 // 反向代理配置 (匹配的 match 如: '/public2', '/public' 要注意顺序) proxyOption: [ { host: 'https://java-admin.likeadmin.cn', match: '/adminapi', }, // { // host: 'http://localhost:3001', // match: '/public2', // map: (path) => path.replace('/public2', ''), // }, // { // host: 'http://localhost:3000', // 目标服务器地址 // match: '/public', // match: /^\/public/, // 匹配需要代理的请求路径 // map: (path) => path.replace('/public', ''), // 重写真实请求地址。路径映射,去除 /api 前缀 // // jar: true, // 启用 cookie 支持 // }, ], }, };
开发规范(约定优于配置)
组件引入与使用
在 JS 中,用 大写开头的驼峰
import HeaderContainer from '$elpisWidgets/header-container/header-container.vue';在 template 模板中,用【小写 + '-'】连接(传参数,命名都用这个规则)
<header-container :title="'项目列表'"></header-container>
组件传参
组件传参用【'-'】,不用驼峰。
<sub-menu :menu-item="item"></sub-menu>组件接收参数可以用驼峰
const { menuItem } = defineProps(['menuItem']);
方法命名规范
onXxx:用户点击的事件方法(如:onMenuSelect)- 比如做埋点的时候,不是用户点击而是逻辑中调用的,那么记录就有问题了
handleXxx:逻辑中调用方法(如:handleMenuSelect)
后端定义路由
- 渲染页面的用
/view/开头: router.get('/view/:page', viewController.renderPage.bind(viewController)); - 接口用
/api/开头:router.post('/api/auth/login', authController.login.bind(authController));- 如果携带 proj_key 的业务 api 要用
/api/proj/开头: router.get('/api/proj/user', userController.get.bind(userController));
- 如果携带 proj_key 的业务 api 要用
前端构建部分的目录规范如下:
- 自定义扩展页需在
app/pages/目录下创建页面入口文件; - 其他组件相关的扩展开发完成后,只需在对应目录的配置文件中添加组件配置,即可被 elpis 前端构建流程一并处理;
- 模板配置则统一放置于
model/目录下。
pages 的目录结构
pages 里所有 entry 开头的 js 文件都是多页面。
多页面进去以后每一个页面渲染后都是用 vue 启动的 vue 实例,所以进来以后又是一个 spa 的单页面应用。
- 比如进入到 dashboard 页面,又因为 dashboard 是用 boot 方法来启动的(用了 vue3 的 createApp 构建出来的),所以它又是一个单页面应用
子模块/子组件,统一放到 complex-view 下面
- 子模块的内容与该页面是强相关,所以放在同一个页面下。全局使用的组件放在 widgets 下。
|--pages |--dashboard |--complex-view |--header-view // 子模块父文件夹 |--asserts // 里面放只应用在该子模块的资源 |--xxx.png |--header-view.vue // 必须用一个父文件夹包裹。因为把资源单独放在了通用的资源文件夹里,容易在删除整个模块时漏删了。 |--dashboard.vue |--entry.dashboard.js
model 模型配置的目录结构
// modelList => projectList => menuList
|--app
|--model
|-- xxx(自定义的模型名称,如buiness/people)
|-- model.js (基类。需要放在自定义的model模型目录下)
|-- project
|-- xxx.js (子类。如jd.js/pdd.js。项目文件必须放在project目录下)提交规范
<type>(<scope>): <subject>feat:新功能(Feature)的添加。fix:Bug 修复docs: 文档 (documentation)的修改style:不影响代码逻辑的样式(如空格、格式、标点等)修改。refactor:代码重构,既不添加新功能也不修复 Bug。perf:性能优化。test:添加测试用例或修改现有测试。build:与构建系统或外部依赖项相关的更改。ci:持续集成(Continuous Integration)配置或脚本的更改。chore:其他不归属上述类型的杂项事务,例如更新构建工具的版本。
文章参考:
Elpis - 基于 Koa + Vue3 的企业级全栈应用框架(allen) @allen-zx/elpis
