@woon_san/elpis
v1.0.1
Published
一个基于 Koa、Vue 3、Element Plus 和 JSON Schema 的企业级全栈应用框架。安装 `@woon_san/elpis` 后,可以通过约定目录扩展服务端能力,并使用 model/project 配置快速生成 Dashboard、Schema 表格、搜索栏和表单页面。
Maintainers
Readme
Elpis
一个基于 Koa、Vue 3、Element Plus 和 JSON Schema 的企业级全栈应用框架。安装 @woon_san/elpis 后,可以通过约定目录扩展服务端能力,并使用 model/project 配置快速生成 Dashboard、Schema 表格、搜索栏和表单页面。
主要能力
- 自动加载业务项目中的
config、service、controller、middleware、router-schema、router和extend。 - 内置 Dashboard、顶部/侧边菜单、iframe 页面和 JSON Schema 驱动的增删改查页面。
- 支持 model 与 project 分层配置:model 提供通用能力,project 只覆盖差异部分。
- 内置开发环境 HMR 和生产环境 Webpack 构建。
- 支持自定义 Vue 页面、Dashboard 路由、动态组件、表单控件和搜索控件。
文档导航
安装
npm install @woon_san/elpis也可以使用其他包管理器:
pnpm add @woon_san/elpis
# 或
yarn add @woon_san/elpis当前包使用 CommonJS 导出,Node.js 项目可直接通过 require() 引入:
const {
serverStart,
frontendBuild,
Controller,
Service
} = require('@woon_san/elpis');Elpis 会以
process.cwd()作为业务项目根目录。请始终在业务项目根目录执行构建和启动命令,否则框架将无法找到app/、config/和model/。
快速开始
下面的示例会创建一个最小的用户列表 Dashboard。
1. 准备项目结构
my-elpis-app/
├── app/
│ ├── controller/
│ │ └── user.js
│ ├── router/
│ │ └── user.js
│ ├── router-schema/
│ │ └── user.js
│ ├── service/
│ │ └── user.js
│ ├── public/
│ │ └── static/
│ └── webpack.config.js
├── config/
│ ├── config.default.js
│ ├── config.local.js
│ └── config.prod.js
├── model/
│ └── user-center/
│ ├── model.js
│ └── project/
│ └── demo.js
├── build.js
├── server.js
└── package.jsonapp/public/static/ 可放置 logo.png 和 normalize.css。内置 HTML 模板会引用 /static/logo.png 与 /static/normalize.css;不提供时不会阻止服务启动,但浏览器会出现对应的静态资源 404。
app/webpack.config.js 在暂时没有自定义构建配置时可以导出空对象:
module.exports = {};2. 添加启动和构建入口
server.js:
const { serverStart } = require('@woon_san/elpis');
serverStart({
name: '用户中心',
homePage: '/view/dashboard/schema?proj_key=demo&key=user'
});build.js:
const { frontendBuild } = require('@woon_san/elpis');
frontendBuild(process.env.NODE_ENV || 'local');为了让环境变量命令同时兼容 macOS、Linux 和 Windows,可安装 cross-env:
npm install --save-dev cross-env然后在业务项目的 package.json 中添加:
{
"scripts": {
"dev:web": "cross-env NODE_ENV=local node build.js",
"dev:server": "cross-env NODE_ENV=local node server.js",
"build": "cross-env NODE_ENV=production node build.js",
"start": "cross-env NODE_ENV=production node server.js"
}
}3. 配置 model 和 project
model/user-center/model.js 定义同一类项目共享的页面结构:
module.exports = {
model: 'dashboard',
name: '用户中心',
desc: '用户中心通用模型',
menu: [
{
key: 'user',
name: '用户管理',
menuType: 'module',
moduleType: 'schema',
schemaConfig: {
api: '/api/proj/user',
schema: {
type: 'object',
properties: {
id: {
type: 'string',
label: '用户 ID',
tableOption: { width: 180 }
},
name: {
type: 'string',
label: '姓名',
tableOption: {}
},
role: {
type: 'string',
label: '角色',
tableOption: {}
}
}
},
tableConfig: {
headerButtons: [],
rowButtons: []
},
searchConfig: {},
componentConfig: {}
}
}
]
};model/user-center/project/demo.js 定义具体项目的信息,也可以覆盖 model 中同 key 的菜单项:
module.exports = {
name: '演示项目',
desc: 'Elpis 快速开始示例',
homePage: '/schema?proj_key=demo&key=user'
};这里的 project homePage 是相对 Dashboard 的地址,框架会自动在前面拼接 /view/dashboard;传给 serverStart() 的 homePage 则必须是完整地址。两处配置的语义不要混用。
目录名会成为配置 key:
user-center会被注入为modelKey。demo.js会被注入为项目的key,页面 URL 中的proj_key必须使用该值。
4. 实现列表接口
app/service/user.js:
const { Service } = require('@woon_san/elpis');
module.exports = (app) => {
const BaseService = Service.Base(app);
return class UserService extends BaseService {
list() {
return [
{ id: '1', name: 'Alice', role: 'admin' },
{ id: '2', name: 'Bob', role: 'member' }
];
}
};
};app/controller/user.js:
const { Controller } = require('@woon_san/elpis');
module.exports = (app) => {
const BaseController = Controller.Base(app);
return class UserController extends BaseController {
list(ctx) {
const data = app.service.user.list();
this.success(ctx, data, {
total: data.length,
page: Number(ctx.request.query.page),
size: Number(ctx.request.query.size)
});
}
};
};app/router/user.js:
module.exports = (app, router) => {
const userController = app.controller.user;
router.get(
'/api/proj/user/list',
userController.list.bind(userController)
);
};app/router-schema/user.js。URL query 在 Koa 中是字符串,因此这里将 page 和 size 声明为 string:
module.exports = {
'/api/proj/user/list': {
get: {
query: {
type: 'object',
properties: {
page: { type: 'string' },
size: { type: 'string' }
},
required: ['page', 'size']
}
}
}
};配置文件至少需要导出一个对象。公共配置会先加载,环境配置随后进行浅层覆盖:
// config/config.default.js
module.exports = {
database: {
client: 'mysql'
}
};// config/config.local.js
module.exports = {
debug: true
};// config/config.prod.js
module.exports = {
debug: false
};5. 启动开发环境
开发时需要同时运行前端构建服务和 Koa 服务。
终端一:
npm run dev:web终端二:
npm run dev:serverWebpack 首次编译完成后访问:
http://localhost:8080/view/dashboard/schema?proj_key=demo&key=user对外 API
| 导出 | 说明 |
| --- | --- |
| serverStart(options) | 创建并立即启动 Koa 服务,返回 Koa app 实例。 |
| frontendBuild(env) | 启动本地前端构建服务或执行生产构建。只处理 local 和 production。 |
| Controller.Base(app) | 返回 Controller 基类,提供统一响应方法。 |
| Service.Base(app) | 返回 Service 基类,提供 app、config 和 curl。 |
serverStart(options)
const { serverStart } = require('@woon_san/elpis');
const app = serverStart({
name: '运营后台',
homePage: '/view/dashboard/schema?proj_key=demo&key=user'
});常用 options:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| name | string | 页面标题,渲染模板时传给前端。 |
| homePage | string | 未匹配路由或模板不存在时的重定向地址。建议提供完整 path 和 query。 |
服务监听地址通过环境变量配置,而不是通过 options:
| 环境变量 | 默认值 | 说明 |
| --- | --- | --- |
| NODE_ENV | 未设置时 app.env.get() 返回 local | 支持 local、beta、production。决定服务端加载哪个环境配置。 |
| PORT | 8080 | Koa 服务端口。 |
| IP | 0.0.0.0 | Koa 服务监听地址。 |
建议始终显式设置 NODE_ENV。虽然未设置时 app.env.get() 返回 local,但只有显式设为 local 时,isLocal() 才为 true,并会加载 config/config.local.js。
启动完成后,常用实例字段包括:
app.options:传给serverStart()的 options。app.config:合并后的业务配置。app.env:环境判断方法,包括get()、isLocal()、isBeta()、isProduction()。app.service、app.controller、app.middlewares:自动加载的业务模块。app.routerSchema:合并后的接口 JSON Schema。app.logger:内置日志扩展。
frontendBuild(env)
const { frontendBuild } = require('@woon_san/elpis');
frontendBuild('local');
// 或
frontendBuild('production');| env | 行为 | 产物/服务 |
| --- | --- | --- |
| local | 启动 Webpack dev middleware 和 HMR。进程会持续运行。 | HMR 默认监听 127.0.0.1:9002,模板写入 app/public/dist/。 |
| production | 执行压缩构建。 | 静态资源写入 app/public/dist/prod/,模板写入 app/public/dist/。 |
frontendBuild('beta') 不会触发构建。beta 环境部署时,先使用 frontendBuild('production') 生成前端资源,再以 NODE_ENV=beta 启动服务端。
服务端扩展
Elpis 会先加载框架内置模块,再加载消费项目中的业务模块。文件名和目录名中的 -、_ 会转换成驼峰命名。
| 业务目录 | 导出格式 | 挂载位置示例 |
| --- | --- | --- |
| app/service/order-service.js | (app) => ServiceClass | app.service.orderService |
| app/controller/admin/user.js | (app) => ControllerClass | app.controller.admin.user |
| app/middleware/auth-check.js | (app) => koaMiddleware | app.middlewares.authCheck |
| app/router/user.js | (app, router) => void | 直接注册到 Koa Router |
| app/router-schema/user.js | JSON Schema 映射对象 | 合并到 app.routerSchema |
| app/extend/cache-client.js | (app) => any | app.cacheClient |
Controller 基类
Controller.Base(app) 提供:
this.success(ctx, data, metadata);
// => { success: true, data, metadata }
this.fail(ctx, message, code);
// => { success: false, message, code }基类实例还可以访问 this.app 和 this.config。
Service 基类
Service.Base(app) 的实例包含:
this.app:Koa app。this.config:app.config。this.curl:superagent,用于在 Service 中发送 HTTP 请求。
全局业务中间件
除 app/middleware/*.js 的可复用中间件外,还可以创建 app/middleware.js,统一决定启用顺序:
module.exports = (app) => {
app.use(app.middlewares.authCheck);
};业务全局中间件在 Elpis 内置全局中间件之后、路由注册之前加载。
路由参数校验
路由 Schema 可分别声明 headers、body、query 和 params。动态路由的 key 必须与 Router 路径一致:
module.exports = {
'/api/proj/user/:id': {
get: {
params: {
type: 'object',
properties: {
id: { type: 'string' }
},
required: ['id']
}
}
}
};module.exports = (app, router) => {
router.get('/api/proj/user/:id', async (ctx) => {
// 参数校验中间件匹配成功后可通过 ctx.params.id 读取。
ctx.body = {
success: true,
data: { id: ctx.params.id }
};
});
};内置 API 约定
项目配置接口
Dashboard 会自动调用以下内置接口。它们同样需要 API 签名:
| 接口 | 参数 | 说明 |
| --- | --- | --- |
| GET /api/project | 必填 query:proj_key | 返回合并继承后的单个 project 完整配置。 |
| GET /api/project/list | 可选 query:proj_key | 不传时返回全部项目;传入后返回该 project 所属 model 下的项目列表。 |
| GET /api/project/model_list | 无 | 返回 model 及其 project 的摘要列表。 |
请求头
所有包含 /api 的请求都会经过签名校验:
s_t:当前毫秒时间戳,有效期 10 分钟。s_sign:md5("sd1bjh21jh2g3kh1hh1h_" + s_t)。
所有 /api/proj/* 请求还必须提供:
proj_key:当前 project 的 key。
Elpis 内置前端请求工具会自动生成这些请求头。自定义浏览器页面只要使用 $elpisCurl,并确保页面 URL 带有 proj_key,通常不需要手动处理:
import $curl from '$elpisCurl';
export async function loadUsers() {
return $curl({
method: 'get',
url: '/api/proj/user/list',
query: { page: 1, size: 20 }
});
}从外部客户端调用时,需要自行生成签名。下面是 Node.js 18+ 的示例:
const { createHash } = require('node:crypto');
async function main() {
const timestamp = Date.now();
const signKey = 'sd1bjh21jh2g3kh1hh1h';
const signature = createHash('md5')
.update(`${signKey}_${timestamp}`)
.digest('hex');
const response = await fetch(
'http://localhost:8080/api/proj/user/list?page=1&size=20',
{
headers: {
s_t: String(timestamp),
s_sign: signature,
proj_key: 'demo'
}
}
);
console.log(await response.json());
}
main().catch(console.error);当前签名 key 是 SDK 内置固定值,只能作为基础请求校验,不能替代登录态、权限控制、HTTPS 或服务端密钥管理。生产项目应在业务中间件中补充正式的认证和授权策略。
响应结构和错误码
成功响应:
{
"success": true,
"data": {},
"metadata": {}
}失败响应:
{
"success": false,
"message": "错误信息",
"code": 442
}| code | 场景 |
| --- | --- |
| 442 | JSON Schema 参数校验失败。 |
| 445 | API 签名缺失、错误或已过期。 |
| 446 | /api/proj/* 请求缺少 proj_key。 |
| 50000 | 未捕获异常或业务通用失败。 |
内置错误处理中,这些业务错误通常仍返回 HTTP 200。客户端应优先判断响应体中的 success 和 code。
model 与 project 配置
目录和继承规则
model/
└── commerce/
├── model.js
└── project/
├── jd.js
└── taobao.jsmodel.js定义同类项目共享的 Dashboard 和菜单配置。project/*.js定义具体项目,并继承对应的model.js。- 普通对象会递归合并。
- 数组元素通过
key匹配:同 key 的元素递归覆盖,只在 model 中存在的元素会继承,只在 project 中存在的元素会追加。 - 建议所有菜单项都提供唯一且稳定的
key,否则无法可靠覆盖。
顶层字段
| 字段 | 说明 |
| --- | --- |
| model | 页面模板类型,当前主要使用 dashboard。 |
| name | model 或 project 名称。 |
| desc | 描述信息。 |
| icon | 可选图标信息。 |
| homePage | project 的默认页面地址,相对 /view/dashboard 配置,例如 /schema?proj_key=demo&key=user。 |
| menu | Dashboard 菜单列表。 |
菜单类型
每个菜单项都需要 key、name 和 menuType。
| menuType / moduleType | 配置字段 | 用途 |
| --- | --- | --- |
| group | subMenu | 顶部菜单分组,可递归包含菜单。 |
| module + sider | siderConfig.menu | 显示带左侧菜单的模块。侧边菜单不能再嵌套 sider。 |
| module + iframe | iframeConfig.path | 在 iframe 中打开页面。 |
| module + custom | customConfig.path | 跳转到业务自定义 Dashboard 路由。 |
| module + schema | schemaConfig | 渲染 Schema 搜索、表格和动态组件。 |
示例:
module.exports = {
model: 'dashboard',
name: '运营后台',
menu: [
{
key: 'content-group',
name: '内容管理',
menuType: 'group',
subMenu: [
{
key: 'article',
name: '文章',
menuType: 'module',
moduleType: 'schema',
schemaConfig: {
api: '/api/proj/article',
schema: { type: 'object', properties: {} }
}
}
]
},
{
key: 'docs',
name: '外部文档',
menuType: 'module',
moduleType: 'iframe',
iframeConfig: {
path: 'https://example.com/docs'
}
}
]
};使用 iframe 时,目标网站必须允许被嵌入;如果对方设置了 X-Frame-Options 或限制性 CSP,浏览器会拒绝显示。
Schema 页面
schemaConfig.api 是资源 API 的基础路径。内置组件会按照以下约定请求:
| 功能 | 请求 |
| --- | --- |
| 表格列表 | GET ${api}/list?page=...&size=... |
| 查看详情 | GET ${api}?<mainKey>=... |
| 新增 | POST ${api} |
| 修改 | PUT ${api} |
| 删除 | DELETE ${api} |
字段配置由标准 JSON Schema 字段和各视图的 *Option 组成:
const schema = {
type: 'object',
properties: {
product_name: {
type: 'string',
label: '商品名称',
minLength: 2,
maxLength: 50,
tableOption: {
width: 240,
'show-overflow-tooltip': true
},
searchOption: {
comType: 'input',
default: ''
},
createFormOption: {
comType: 'input',
default: ''
},
editFormOption: {
comType: 'input'
},
detailPanelOption: {}
},
price: {
type: 'number',
label: '价格',
minimum: 0,
tableOption: {
width: 120,
toFixed: 2
},
searchOption: {
comType: 'select',
enumList: [
{ label: '全部', value: '' },
{ label: '100 元', value: 100 }
]
},
createFormOption: {
comType: 'inputNumber'
},
editFormOption: {
comType: 'inputNumber'
},
detailPanelOption: {}
}
},
required: ['product_name']
};内置控件:
| Option | 内置 comType |
| --- | --- |
| searchOption | input、select、dynamicSelect、dateRange |
| createFormOption / editFormOption | input、inputNumber、select |
| tableOption | 透传 Element Plus el-table-column 配置,并支持 visible、toFixed。 |
| detailPanelOption | 字段存在时,会在详情抽屉中展示。 |
当 searchOption.comType 为 dynamicSelect 时,用 api 指定枚举接口。接口应返回 { success: true, data: [{ label, value }] }。
表格按钮和动态组件
内置事件使用字段名 eventOption(单数):
const tableConfig = {
headerButtons: [
{
label: '新增商品',
eventKey: 'showComponent',
eventOption: { comName: 'createForm' },
type: 'primary'
}
],
rowButtons: [
{
label: '编辑',
eventKey: 'showComponent',
eventOption: { comName: 'editForm' },
type: 'warning'
},
{
label: '删除',
eventKey: 'remove',
eventOption: {
params: {
product_id: 'schema::product_id'
}
},
type: 'danger'
}
]
};showComponent:打开componentConfig中同名组件。remove:按eventOption.params取当前行字段并发送 DELETE 请求。schema::product_id表示从当前表格行读取product_id。
内置动态组件配置:
const componentConfig = {
createForm: {
title: '新增商品',
saveBtnText: '保存'
},
editForm: {
mainKey: 'product_id',
title: '编辑商品',
saveBtnText: '保存修改'
},
detailPanel: {
mainKey: 'product_id',
title: '商品详情'
}
};只有同时满足以下条件时,对应组件才会出现:
componentConfig中声明了组件。- Schema 字段配置了对应的
createFormOption、editFormOption或detailPanelOption。 - 按钮的
eventOption.comName与组件名一致。
前端扩展
独立页面入口
在 app/pages/ 任意子目录创建 entry.<page>.js,构建后可通过 /view/<page> 访问。
// app/pages/report/entry.report.js
import boot from '$elpisBoot';
import ReportPage from './report-page.vue';
boot(ReportPage);对应地址:
http://localhost:8080/view/report如果需要 Vue Router:
import boot from '$elpisBoot';
import ReportPage from './report-page.vue';
import ReportDetail from './report-detail.vue';
boot(ReportPage, {
routes: [
{
path: '/view/report/:id',
component: ReportDetail
}
]
});Dashboard 自定义路由
创建 app/pages/dashboard/router.js:
module.exports = ({ routes }) => {
routes.push({
path: '/view/dashboard/report',
component: () => import('./report/report.vue')
});
};model 中添加菜单:
const reportMenu = {
key: 'report',
name: '数据报表',
menuType: 'module',
moduleType: 'custom',
customConfig: {
path: '/report'
}
};customConfig.path 会拼接在 /view/dashboard 后面,因此必须与自定义 route 的 path 保持一致。
扩展动态组件和控件
| 扩展目标 | 业务配置文件 |
| --- | --- |
| Schema View 动态组件 | app/pages/dashboard/complex-view/schema-view/components/component-config.js |
| Schema Form 表单控件 | app/pages/widgets/schema-form/form-item-config.js |
| Schema Search Bar 搜索控件 | app/pages/widgets/schema-search-bar/search-item-config.js |
配置文件导出“组件名到组件”的映射:
import UserPicker from './complex-view/user-picker/user-picker.vue';
export default {
userPicker: {
component: UserPicker
}
};之后在 Schema 中使用同名 comType:
const properties = {
assignee: {
type: 'string',
label: '负责人',
createFormOption: {
comType: 'userPicker'
}
}
};自定义表单控件需要接收 schemaKey、schema、model,并通过 defineExpose() 暴露 validate() 和 getValue();自定义搜索控件需要暴露 getValue() 和 reset()。
自定义 Schema View 动态组件需要通过 defineExpose() 暴露 name 和 show(rowData);保存数据后可触发 command 事件并传入 { event: 'loadTableData' },通知内置表格重新加载。
自定义 Webpack 配置
业务项目可以创建 app/webpack.config.js,该配置会与框架基础配置合并:
const path = require('node:path');
module.exports = {
resolve: {
alias: {
'@business': path.resolve(process.cwd(), 'app/pages')
}
}
};生产构建与部署
npm run build
npm run start部署时请确保:
- 构建和启动命令都在业务项目根目录执行。
app/public/dist/已生成并随应用一起部署。- 运行环境已设置
NODE_ENV=production、PORT和IP。 - 反向代理会转发页面、
/api/*、/dist/*和/static/*。 logs/目录可写;非 local 环境的日志会写入logs/application.log并按日期切分。- 生产服务已补充业务所需的认证、授权、密钥和数据库安全配置。
常见问题
启动后页面被重定向或提示模板不存在
先执行前端构建,确认 app/public/dist/entry.dashboard.tpl 或对应的 entry.<page>.tpl 已生成,并检查传给 serverStart() 的 homePage 是否以 /view/... 开头。
页面能打开,但项目配置为空
检查 URL 是否包含正确的 proj_key,并确认它与 model/<model-key>/project/<proj_key>.js 的文件名一致。
API 返回 442
请求没有通过 router-schema 校验。注意 query 和 path 参数通常是字符串;body 中的数值才会按 JSON number 传递。
API 返回 445
检查 s_t、s_sign 是否存在,时间戳是否在 10 分钟内,以及签名字符串是否使用了正确格式。内置 $elpisCurl 会自动处理签名。
API 返回 446
/api/proj/* 请求缺少 proj_key。内置前端会从页面 URL 的 proj_key 初始化该请求头,因此首先检查访问地址。
修改了目录或文件名,但无法通过 app 访问
确认文件位于约定目录、导出格式正确,并从业务项目根目录启动。user-service.js 会挂载为 userService,嵌套目录会形成嵌套对象。
License
ISC
