Elpis - 基于 Koa + Vue3 的企业级全栈应用框架
前言
对于大多数中后台开发者而言,日常开发的核心工作往往聚焦于业务逻辑的CRUD实现——无论是简单的基础功能还是复杂的业务场景,本质上都是对数据的增删改查操作,存在大量重复性劳动。 全栈领域模型框架elpis正是为解决这一痛点而生。我们通过模型驱动开发(MDD) 对业务模型进行抽象建模,创新性地实现了:
- 标准化描述体系 - 通过统一模型定义前端UI界面、后端API接口及数据库Schema
-
自动化生成引擎 - 内置三大核心引擎:
- 前端UI渲染引擎:动态生成界面
- 后端接口生成引擎:自动输出RESTful API
- 数据库建表引擎:智能构建数据存储层
- 全链路协同 - 模型变更实时同步至前后端及数据库,确保系统一致性 这种模型驱动的开发范式,将传统CRUD的开发效率提升10倍以上,让开发者能够专注于真正的业务创新而非重复编码。
elpis npm包下载使用:点击这里
1. elpis-core 设计
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应用,保持前端交互体验一致性
- 模板引擎:智能生成差异化页面模板,自动注入代码块(如埋点、权限校验等)
全生命周期工程化支持
-
生产级构建流水线
- 多环境适配:自动处理ES语法降级、Polyfill注入
- 性能优化:代码分包(Code Splitting)、Tree Shaking、Gzip压缩
- 质量保障:Bundle分析报告、依赖大小可视化
-
极致开发体验
- 热更新(HMR)体系:模块级热替换,保存即生效
- 增量编译:毫秒级响应代码变更
- 调试增强:SourceMap与错误追踪深度集成
-
标准化交付方案
- 框架即产品:完备的npm包发布规范
- 版本管理:语义化版本控制(SemVer)
- 私有化支持:无缝对接企业私有仓库
框架定位
elpis既是开发加速器(通过约定范式提升效率),又是工程规范实施者(通过标准化流程保障质量),最终实现:
- 多系统切换成本降低70%
- 构建效率提升3倍
- 生产环境稳定性达99.9%
2.1 SSR 入口文件的处理
背景痛点
在构建多入口SSR应用时,传统方案需要为每个入口重复编写框架初始化代码(如Vue的createApp
或React的createRoot
),导致:
- 代码冗余度高
- 维护成本增加
- 技术栈升级困难
解决方案
我们设计了标准化应用启动器(Bootloader) ,通过抽象框架初始化逻辑实现:
-
统一初始化接口
- 提供
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: {
// 重复配置...
}
// 随着入口增长,配置急剧膨胀
}
这种模式会导致:
- 配置文件臃肿难维护
- 新增入口需修改核心配置
- 容易产生配置冲突
工程化解决方案
我们采用约定优于配置原则,将入口文件放在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 打包优化
性能挑战
随着项目规模增长,传统构建模式会面临两大核心问题:
-
构建速度指数级下降
- 典型表现:200+模块项目冷启动构建超过8分钟
- 根本原因:未优化的依赖解析和全量编译
-
产物体积失控
- 常见问题:主包超过2MB导致首屏加载缓慢
- 关键因素:未做代码分割和按需加载
系统性解决方案
一、构建加速体系
- 增量编译优化
// webpack配置
cache: {
type: 'filesystem',
buildDependencies: { config: \[\_\_filename] }
}
- 效果:二次构建速度提升70%+
- 多进程处理
// 使用thread-loader并行化
npm install thread-loader --save-dev
- 依赖预编译
externals: {
vue: 'Vue',
lodash: '\_'
}
二、代码瘦身策略
- 智能代码分割
{
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
}
}
- 按需加载
// 动态导入语法
const Login = () => import(/* webpackPrefetch: true */ './Login.vue')
- 高级压缩
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(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配置,结合各个解析器,实现页面渲染
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包:点击这里