npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@woon_san/elpis

v1.0.1

Published

一个基于 Koa、Vue 3、Element Plus 和 JSON Schema 的企业级全栈应用框架。安装 `@woon_san/elpis` 后,可以通过约定目录扩展服务端能力,并使用 model/project 配置快速生成 Dashboard、Schema 表格、搜索栏和表单页面。

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.json

app/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:server

Webpack 首次编译完成后访问:

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.js
  • model.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: '商品详情'
    }
};

只有同时满足以下条件时,对应组件才会出现:

  1. componentConfig 中声明了组件。
  2. Schema 字段配置了对应的 createFormOption、editFormOption 或 detailPanelOption。
  3. 按钮的 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