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

@qinmu-/elpis

v1.0.3

Published

Elpis - 基于 Vue 3 + Koa 2 的全栈 Web 应用.

Readme

Elpis

基于 Vue 3 + Koa 2 的全栈 Web 应用框架

Elpis 是一个约定式全栈框架,提供自动加载、多层配置合并、API 参数校验、MPA 多页前端架构、Schema 驱动视图等能力,帮助开发者快速构建企业级 Web 应用。


目录


核心特性

| 特性 | 说明 | |------|------| | 约定式自动加载 | Controller、Service、Router、Middleware、Extend 按目录约定自动扫描注册 | | 四层配置合并 | 框架默认 → 业务默认 → 框架环境 → 业务环境,链式覆盖 | | 内置安全中间件 | API 签名校验(MD5 + 时间戳)、参数 Schema 校验(Ajv) | | 统一响应格式 | Controller 基类提供 success() / fail() 标准化响应 | | MPA 多页架构 | 前端按页面独立入口,Webpack 自动扫描打包,代码分割复用 | | Schema 驱动视图 | 通过 JSON Schema 自动生成搜索面板、数据表格、表单弹窗 | | Model 继承体系 | 项目(Project)可继承业务模型(Model)配置,支持 deep merge | | HMR 热更新 | Express + webpack-dev-middleware + webpack-hot-middleware | | 前后端分离开发 | 前端开发服务器代理 /api 到 Koa 后端,单端口访问 |

技术栈:

  • 后端:Koa 2、koa-router、koa-bodyparser、superagent、ajv、knex
  • 前端:Vue 3、Element Plus、Pinia、Vue Router、ECharts
  • 构建:Webpack 5、Babel、Less
  • 模板:Nunjucks

环境要求

  • Node.js >= 12.x
  • npm >= 6.x

快速开始

1. 创建项目

mkdir my-project
cd my-project
npm init -y

2. 安装 Elpis

npm install @qinmu-/elpis

3. 创建服务入口文件

在项目根目录创建 server.js:

const { serverStart } = require('@qinmu-/elpis')

const app = serverStart({
  name: 'myApp',
  homePage: '/view/project-list'
})

4. 启动开发服务

启动 Koa 后端(端口 8080):

# macOS / Linux
_ENV='local' nodemon ./server.js

# Windows
set _ENV='local' && node ./server.js

启动前端开发服务器(端口 8081,带 HMR 热更新):

# macOS / Linux
_ENV='local' node -e "require('@qinmu-/elpis').startDevServer({ pagesPath: require('path').resolve(__dirname, 'app') })"

# Windows
set _ENV='local' && node -e "require('@qinmu-/elpis').startDevServer({ pagesPath: require('path').resolve(__dirname, 'app') })"

提示:开发时建议同时启动后端和前端开发服务器。前端开发服务器会将 /api/* 请求代理到 Koa 后端(默认 http://127.0.0.1:8080),实现单端口开发(访问 http://localhost:8081)。


项目结构

一个典型的 Elpis 项目目录结构如下:

my-project/
├── server.js                    # 服务入口,调用 serverStart 启动
├── package.json                 # 项目配置
├── config/                      # 配置文件(四层合并)
│   ├── config.default.js        #   默认配置
│   ├── config.local.js          #   本地环境配置
│   ├── config.beta.js           #   测试环境配置
│   └── config.prod.js           #   生产环境配置
├── app/                         # 业务代码目录
│   ├── middleware.js             #   全局中间件注册入口
│   ├── controller/              #   控制器
│   │   └── *.js
│   ├── service/                 #   服务层
│   │   └── *.js
│   ├── middleware/              #   自定义中间件
│   │   └── *.js
│   ├── router/                  #   路由注册
│   │   └── *.js
│   ├── router-schema/           #   参数校验 Schema
│   │   └── *.js
│   ├── extend/                  #   框架扩展
│   │   └── *.js
│   ├── pages/                   #   前端页面(MPA 多页入口)
│   │   └── {pageName}/
│   │       ├── entry.{name}.js  #     页面入口文件
│   │       └── *.vue            #     页面组件
│   └── public/                  #   静态资源
│       └── dist/                #     构建产物
├── model/                       # 模型配置(菜单/页面定义)
│   └── {bizName}/
│       ├── model.js             #   通用模型
│       └── project/
│           └── {project}.js     #   项目配置(继承 model)
└── logs/                        # 日志目录

各层职责

| 层级 | 目录 | 职责 | |------|------|------| | 配置 | config/ | 环境相关配置,四层链式合并 | | 中间件 | app/middleware/ | 请求预处理(鉴权、日志、参数校验等) | | 参数校验 | app/router-schema/ | 定义 API 的 headers/body/query 参数 Schema | | 路由 | app/router/ | URL 到 Controller 方法的映射 | | 控制器 | app/controller/ | 解析请求参数,调用 Service,返回响应 | | 服务层 | app/service/ | 业务逻辑封装,数据库操作 | | 扩展 | app/extend/ | 扩展 app 实例(如 logger) | | 前端 | app/pages/ | Vue 组件,按页面独立打包 | | 模型 | model/ | 菜单结构、页面 Schema 配置 |


配置指南

四层配置合并

Elpis 采用四层链式合并策略加载配置,优先级从低到高:

第1层: elpis/config/config.default.js       (框架默认配置)
第2层: {项目}/config/config.default.js       (业务默认配置)
第3层: elpis/config/config.{env}.js          (框架环境配置)
第4层: {项目}/config/config.{env}.js         (业务环境配置)  ← 最高优先级

环境映射关系:

| 环境 | _ENV 值 | 配置文件 | |------|-----------|----------| | 本地开发 | local | config.local.js | | 测试环境 | beta | config.beta.js | | 生产环境 | production | config.prod.js |

配置文件示例

config/config.default.js(默认配置):

module.exports = {
  name: 'my-app',
  apiBase: 'http://localhost:8080',
  debug: true,
  pageSize: 10,
}

config/config.local.js(本地环境覆盖):

module.exports = {
  apiBase: 'http://localhost:8080',
  debug: true,
  pageSize: 5,
}

config/config.prod.js(生产环境覆盖):

module.exports = {
  apiBase: 'https://api.example.com',
  debug: false,
  pageSize: 20,
}

配置合并使用 Object.assign,后层同名字段会覆盖前层。


后端开发指南

Controller 控制器

控制器负责解析请求参数、调用 Service 处理业务逻辑、返回统一格式响应。

基类方法

所有 Controller 继承自 BaseController,可直接使用以下方法:

// 成功响应
this.success(ctx, data, metadata)
// 返回: { success: true, metadata: {...}, data: {...} }

// 失败响应
this.fail(ctx, message, code)
// 返回: { success: false, message: '...', code: 400 }

编写 Controller

在 app/controller/ 目录下创建文件,导出工厂函数返回类:

// app/controller/product.js
module.exports = (app) => class ProductController {
  constructor() {
    this.app = app
    this.config = app.config
    // 通过 app.controller 访问其他控制器
    // 通过 app.service 访问服务层
  }

  async getList(ctx) {
    const { page = 1, pageSize = 10, keyword } = ctx.query
    const data = await app.service.product.getList({ page, pageSize, keyword })
    this.success(ctx, data)
  }

  async create(ctx) {
    const params = ctx.request.body
    const data = await app.service.product.create(params)
    this.success(ctx, data)
  }

  async update(ctx) {
    const { id } = ctx.query
    const params = ctx.request.body
    const data = await app.service.product.update(id, params)
    this.success(ctx, data)
  }

  async delete(ctx) {
    const { id } = ctx.query
    const data = await app.service.product.delete(id)
    this.success(ctx, data)
  }
}

自动挂载规则

Controller 按文件路径自动挂载到 app.controller:

| 文件路径 | 挂载路径 | |----------|----------| | app/controller/product.js | app.controller.product | | app/controller/user/auth.js | app.controller.user.auth | | app/controller/custom-module/order.js | app.controller.customModule.order |

连字符 - 和下划线 _ 会自动转换为驼峰命名。业务控制器会覆盖框架同路径控制器。


Service 服务层

Service 封装业务逻辑,基类注入 app、config、curl(superagent)。

基类属性

this.app      // Koa 应用实例
this.config   // 合并后的配置对象
this.curl     // superagent HTTP 客户端

编写 Service

// app/service/product.js
module.exports = (app) => class ProductService {
  constructor() {
    this.app = app
    this.config = app.config
    this.curl = require('superagent')
  }

  async getList({ page = 1, pageSize = 10, keyword = '' }) {
    // 数据库查询、外部 API 调用等业务逻辑
    const result = await this.curl
      .get(`${this.config.apiBase}/external/products`)
      .query({ page, pageSize, keyword })

    return result.body
  }

  async create(params) {
    // 创建逻辑
    return { id: Date.now(), ...params }
  }

  async update(id, params) {
    // 更新逻辑
    return { id, ...params }
  }

  async delete(id) {
    // 删除逻辑
    return { id }
  }
}

Service 的自动挂载规则与 Controller 相同,挂载到 app.service。


Router 路由

在 app/router/ 目录下创建路由文件,导出函数接收 (app, router) 参数:

// app/router/product.js
module.exports = (app, router) => {
  const { productController } = app.controller

  router.get('/api/product', productController.getList.bind(productController))
  router.post('/api/product', productController.create.bind(productController))
  router.put('/api/product', productController.update.bind(productController))
  router.delete('/api/product', productController.delete.bind(productController))
}
// app/router/view.js
module.exports = (app, router) => {
  const { viewController } = app.controller

  // 页面渲染路由,支持 history 模式
  router.get('/view/:page', viewController.renderPage.bind(viewController))
  router.get('/view/:page/*', viewController.renderPage.bind(viewController))
}

注意:Controller 方法通过 .bind(controllerInstance) 绑定 this 上下文。路由在框架内置路由之后注册,业务路由可覆盖框架路由。


Router-Schema 参数校验

定义 API 接口的请求参数 Schema,由 api-params-verify 中间件自动校验。

// app/router-schema/product.js
module.exports = {
  '/api/product': {
    GET: {
      headers: {
        properties: {
          projkey: { type: 'string', required: true, description: '项目标识' }
        }
      },
      query: {
        properties: {
          page: { type: 'number', description: '页码' },
          pageSize: { type: 'number', description: '每页条数' },
          keyword: { type: 'string', description: '搜索关键词' }
        }
      }
    },
    POST: {
      headers: {
        properties: {
          projkey: { type: 'string', required: true }
        }
      },
      body: {
        properties: {
          name: { type: 'string', required: true, description: '商品名称' },
          price: { type: 'number', required: true, description: '价格' },
          category: { type: 'string', description: '分类' }
        }
      }
    },
    PUT: {
      headers: {
        properties: {
          projkey: { type: 'string', required: true }
        }
      },
      query: {
        properties: {
          id: { type: 'string', required: true, description: '商品ID' }
        }
      },
      body: {
        properties: {
          name: { type: 'string', description: '商品名称' },
          price: { type: 'number', description: '价格' }
        }
      }
    },
    DELETE: {
      headers: {
        properties: {
          projkey: { type: 'string', required: true }
        }
      },
      query: {
        properties: {
          id: { type: 'string', required: true, description: '商品ID' }
        }
      }
    }
  }
}

Schema 使用 Ajv 进行校验,校验失败返回:

{
  "success": false,
  "code": 442,
  "message": "request parameters fail: ..."
}

Middleware 中间件

全局中间件注册

在 app/middleware.js 中按洋葱圈模型注册全局中间件:

// app/middleware.js
module.exports = (app) => {
  const { errorHandler, apiSignVerify, apiParamsVerify, projectHandler } = app.middlewares

  app.use(errorHandler)
  app.use(apiSignVerify)
  app.use(apiParamsVerify)
  app.use(projectHandler)

  // 注册自定义中间件
  app.use(app.middlewares.customMiddleware)
}

编写自定义中间件

// app/middleware/custom.js
module.exports = (app) => async (ctx, next) => {
  const start = Date.now()
  console.log(`[${ctx.method}] ${ctx.url} - 请求开始`)

  await next()

  const ms = Date.now() - start
  console.log(`[${ctx.method}] ${ctx.url} - 响应完成 (${ms}ms)`)
}

内置中间件说明

| 中间件 | 文件 | 说明 | |--------|------|------| | error-handler | app/middleware/error-handler.js | 全局异常捕获,统一错误响应格式 | | api-sign-verify | app/middleware/api-sign-verify.js | API 签名校验(MD5 + 10分钟时间窗口),仅 /api 路径 | | api-params-verify | app/middleware/api-params-verify.js | 基于 Router-Schema 的 Ajv 参数校验,仅 /api 路径 | | project-handler | app/middleware/project-handler.js | 从请求头提取 projKey,注入 ctx.projKey,仅 /api 路径 |

API 签名规则:

  • 前端请求需携带请求头 s_t(时间戳)和 s_sign(签名)
  • 签名算法:md5('ssadklkslflsdlfljasjksd' + '_' + s_t)
  • 时间差超过 10 分钟则签名失效

Extend 扩展

扩展用于向 app 实例直接挂载属性或方法:

// app/extend/logger.js
const log4js = require('log4js')

module.exports = (app) => {
  log4js.configure({
    appenders: { app: { type: 'file', filename: 'logs/app.log' } },
    categories: { default: { appenders: ['app'], level: 'info' } }
  })

  const logger = log4js.getLogger('app')

  return {
    info: (msg) => logger.info(msg),
    error: (msg) => logger.error(msg),
    warn: (msg) => logger.warn(msg),
  }
}

挂载后通过 app.logger.info('message') 使用。扩展会直接挂载到 app 根属性,同名属性会触发警告提示。


前端开发指南

MPA 多页架构

Elpis 前端采用 MPA(Multi-Page Application)架构,每个页面独立入口、独立打包。

app/pages/
├── boot.js                      # 统一启动入口(跨页面复用,自动抽到 common.js)
├── common/                      # 公共模块
│   ├── curl.js                  #   axios 请求封装
│   ├── utils.js                 #   公共工具函数
│   └── stores/                  #   公共 Store
├── store/                       # 页面级 Store
│   ├── dashboard.js
│   └── user.js
├── dashboard/                   # Dashboard 页面
│   ├── entry.dashboard.js       #   入口文件(必需,命名规则:entry.{name}.js)
│   ├── dashboard.vue            #   根组件
│   └── ...                      #   子组件/子页面
└── project-list/                # 项目列表页面
    ├── entry.project.js         #   入口文件
    └── project-list.vue         #   根组件

入口文件命名规则:entry.{name}.js,Webpack 会自动扫描 pages/*/entry.*.js 作为构建入口。


boot.js 统一启动

boot.js 提供统一的 Vue 应用启动能力,自动装配 Pinia、Element Plus、Vue Router:

// app/pages/boot.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import { createRouter, createWebHistory } from 'vue-router'

export function boot(rootComponent, options = {}) {
  const app = createApp(rootComponent)

  // 装配 Pinia
  const pinia = createPinia()
  app.use(pinia)

  // 装配 Element Plus
  app.use(ElementPlus)

  // 可选装配 Vue Router
  if (options.routes) {
    const router = createRouter({
      history: createWebHistory(`/view/${options.pageName}`),
      routes: options.routes,
    })
    app.use(router)
  }

  app.mount('#app')
  return app
}

页面入口文件示例:

// app/pages/dashboard/entry.dashboard.js
import { boot } from '../boot'
import Dashboard from './dashboard.vue'

boot(Dashboard, {
  pageName: 'dashboard',
  routes: [
    { path: '/', redirect: '/todo' },
    { path: '/todo', component: () => import('./todo/todo.vue') },
    { path: '/schema', component: () => import('./complex-view/schema-view/schema-view.vue') },
    { path: '/sider/:chapters+', component: () => import('./complex-view/sider-view/sider-view.vue') },
  ]
})

curl.js 请求层

curl.js 是对 axios 的二次封装,分为 HTTP 层和业务层:

HTTP 层(axios 拦截器)

自动注入请求头:

  • s_t:当前时间戳
  • s_sign:MD5 签名(md5('ssadklkslflsdlfljasjksd' + '_' + s_t))
  • Authorization:JWT Token
  • projKey:当前选中项目标识(从 localStorage 读取)

业务层(curl 方法)

按后端统一格式 { success, metadata, data } 自动解包:

import curl from '@/common/curl'

// 成功时直接返回 data
const data = await curl.get('/api/product', { params: { page: 1 } })
// data = { list: [...], total: 100 }

// 失败时抛出包含 code/message/metadata 的 Error
try {
  await curl.post('/api/product', { name: 'test' })
} catch (err) {
  console.log(err.code)     // 错误码
  console.log(err.message)  // 错误信息
}

可用方法:curl.get()、curl.post()、curl.put()、curl.delete()


Store 状态管理

使用 Pinia 管理状态,在 app/pages/store/ 目录下创建:

// app/pages/store/user.js
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  state: () => ({
    username: '',
    token: '',
  }),
  actions: {
    login(username, token) {
      this.username = username
      this.token = token
    },
    logout() {
      this.username = ''
      this.token = ''
    },
  },
})

在组件中使用:

import { useUserStore } from '@/store/user'

const userStore = useUserStore()
userStore.login('admin', 'xxx-token')

Schema 驱动视图

Schema 驱动视图是 Elpis 的核心前端能力,通过一份 JSON Schema 配置即可自动生成搜索面板、数据表格和表单弹窗。

Schema 配置结构

const schemaConfig = {
  // 搜索面板配置
  searchFields: [
    { name: 'keyword', label: '关键词', type: 'STRING' },
    { name: 'category', label: '分类', type: 'SELECT', options: [
      { label: '电子产品', value: 'electronics' },
      { label: '服装', value: 'clothing' },
    ]},
    { name: 'createTime', label: '创建时间', type: 'DATE' },
  ],

  // 表格列配置
  tableColumns: [
    { name: 'id', label: 'ID', width: 80 },
    { name: 'name', label: '商品名称' },
    { name: 'price', label: '价格', sortable: true },
    { name: 'category', label: '分类' },
    { name: 'status', label: '状态' },
  ],

  // 操作按钮
  actions: {
    top: [
      { name: 'create', label: '新增', type: 'primary' },
    ],
    row: [
      { name: 'edit', label: '编辑' },
      { name: 'delete', label: '删除', type: 'danger' },
    ],
  },

  // 表单配置
  formFields: [
    { name: 'name', label: '商品名称', type: 'STRING', required: true },
    { name: 'price', label: '价格', type: 'NUMBER', required: true },
    { name: 'category', label: '分类', type: 'SELECT', options: [
      { label: '电子产品', value: 'electronics' },
      { label: '服装', value: 'clothing' },
    ]},
  ],

  // API 配置
  api: {
    list: '/api/product',
    create: '/api/product',
    update: '/api/product',
    delete: '/api/product',
  },
}

支持的表单控件类型

| 类型 | 常量 | 渲染控件 | |------|------|----------| | STRING | 文本输入 | el-input | | NUMBER | 数字输入 | el-input-number | | SELECT | 下拉选择 | el-select | | DATE | 日期选择 | el-date-picker |


公共组件

框架内置了两个布局容器组件:

| 组件 | 路径 | 说明 | |------|------|------| | header-container | pages/widgets/header-container/ | 顶部导航栏布局,含 el-menu 和项目选择器 | | sider-container | pages/widgets/sider-container/ | 侧边栏布局,含可折叠菜单 |

在页面中使用:

<template>
  <header-container>
    <router-view />
  </header-container>
</template>

<script setup>
import HeaderContainer from '@/widgets/header-container/header-container.vue'
</script>

Model 模型层

Model 层用于定义业务模型和项目配置,支持项目继承模型的 deep merge 机制。

目录结构

model/
├── index.js                     # 模型解析器
├── buiness/                     # 电商业务线
│   ├── model.js                 #   通用模型
│   └── project/
│       ├── jd.js                #   京东项目
│       ├── pdd.js               #   拼多多项目
│       └── taobao.js            #   淘宝项目
└── course/                      # 课程业务线
    ├── model.js                 #   通用模型
    └── project/
        ├── bilibili.js          #   B站课堂
        └── douyin.js            #   抖音课堂

模型配置

// model/buiness/model.js - 通用模型
module.exports = {
  mode: 'dashboard',
  menuList: [
    { name: 'product', label: '商品管理', type: 'custom' },
    {
      name: 'order', label: '订单管理',
      type: 'schema',
      schemaConfig: {
        searchFields: [ /* ... */ ],
        tableColumns: [ /* ... */ ],
        api: { list: '/api/order' },
      },
    },
    { name: 'customer', label: '客户管理', type: 'custom' },
  ],
}
// model/buiness/project/jd.js - 京东项目(继承通用模型)
module.exports = {
  menuList: [
    // 覆盖商品管理
    { name: 'product', label: '京东商品管理', type: 'custom' },
    // 新增营销中心(分组菜单)
    {
      name: 'marketing', label: '营销中心', type: 'group',
      children: [
        { name: 'coupon', label: '优惠券', type: 'custom' },
        { name: 'seckill', label: '秒杀', type: 'custom' },
      ],
    },
    // 新增数据分析(侧边栏模式)
    {
      name: 'analytics', label: '数据分析', type: 'sider',
      children: [
        { name: 'sales-report', label: '销售报表', type: 'custom' },
        { name: 'user-portrait', label: '用户画像', type: 'custom' },
      ],
    },
  ],
}

菜单类型

| 类型 | 说明 | |------|------| | custom | 自定义页面(需自行开发 Vue 组件) | | schema | Schema 驱动页面(自动生成搜索/表格/表单) | | iframe | iframe 嵌入外部页面 | | group | 分组菜单(顶部下拉子菜单) | | sider | 侧边栏菜单(带子路由) |

继承机制

项目配置通过 deep merge 继承模型配置,同名 key 项目覆盖模型,模型中有而项目中没有的配置保留。


构建与部署

环境变量

通过 _ENV 环境变量控制运行环境:

| 命令 | 环境 | |------|------| | _ENV='local' | 本地开发 | | _ENV='beta' | 测试环境 | | _ENV='production' | 生产环境 |

开发命令

# 启动 Koa 后端
_ENV='local' node ./server.js

# 启动前端开发服务器(HMR 热更新,端口 8081)
_ENV='local' node -e "require('@qinmu-/elpis').startDevServer({ pagesPath: require('path').resolve(__dirname, 'app') })"

生产构建

# 前端生产构建
_ENV='production' node -e "require('@qinmu-/elpis').buildApp({ pagesPath: require('path').resolve(__dirname, 'app') })"

构建产物输出到 app/public/ 目录:

  • app/public/dist/ — HTML 模板文件
  • app/public/static/js/ — JS bundle(带 contenthash)
  • app/public/static/css/ — CSS 文件

生产部署

# 启动生产服务
_ENV='production' node ./server.js

Windows 命令

Windows 环境下使用 set 命令:

# 开发
set _ENV='local' && node ./server.js
set _ENV='local' && node -e "require('@qinmu-/elpis').startDevServer({ pagesPath: require('path').resolve(__dirname, 'app') })"

# 构建
set _ENV='production' && node -e "require('@qinmu-/elpis').buildApp({ pagesPath: require('path').resolve(__dirname, 'app') })"

# 生产
set _ENV='production' && node ./server.js

package.json 脚本配置

推荐在业务项目的 package.json 中配置便捷脚本:

{
  "scripts": {
    "dev": "_ENV='local' nodemon ./server.js",
    "dev:win": "set _ENV='local' && node ./server.js",
    "prod": "_ENV='production' node ./server.js",
    "prod:win": "set _ENV='production' && node ./server.js",
    "build": "_ENV='production' node -e \"require('@qinmu-/elpis').buildApp({ pagesPath: require('path').resolve(__dirname, 'app') })\"",
    "build:win": "set _ENV='production' && node -e \"require('@qinmu-/elpis').buildApp({ pagesPath: require('path').resolve(__dirname, 'app') })\"",
    "build:watch": "_ENV='local' node -e \"require('@qinmu-/elpis').buildApp({ pagesPath: require('path').resolve(__dirname, 'app'), watch: true })\"",
    "dev:fe": "_ENV='local' node -e \"require('@qinmu-/elpis').startDevServer({ pagesPath: require('path').resolve(__dirname, 'app') })\""
  }
}

Webpack 构建特性

| 特性 | 开发模式 | 生产模式 | |------|----------|----------| | Mode | development | production | | Source Map | eval-cheap-module-source-map | 关闭 | | 文件名 | 无 hash | [contenthash:8] | | CSS 提取 | vue-style-loader(内联) | MiniCssExtractPlugin(独立文件) | | 代码压缩 | 不压缩 | TerserPlugin + CssMinimizerPlugin | | 构建缓存 | 无 | filesystem 持久化缓存 | | 构建清理 | 无 | CleanWebpackPlugin | | console 移除 | 保留 | drop_console: true |

代码分割策略:

  • vendors:所有 node_modules 依赖打包为 vendors.js
  • common:被 2 个以上入口引用的公共模块(如 boot.js)抽取为 common.js

框架 API 参考

serverStart(options)

启动 Elpis 服务。

const { serverStart } = require('@qinmu-/elpis')
const app = serverStart(options)

参数:

| 参数 | 类型 | 说明 | |------|------|------| | options.name | string | 应用名称 | | options.homePage | string | 首页路径,如 '/view/project-list' |

返回值:Koa 应用实例。

内部流程:

  1. 创建 Koa 实例
  2. 设置 app.baseDir 为 process.cwd()
  3. 设置 app.businessPath 为 {cwd}/app
  4. 初始化环境变量(app.env)
  5. 依次加载配置、中间件、Router-Schema、Service、Controller、Extend
  6. 注册全局中间件(框架 + 业务)
  7. 注册路由
  8. 监听端口(默认 8080,可通过 PORT 和 HOST 环境变量配置)

BaseService

Service 基类,供业务 Service 继承。

const { BaseService } = require('@qinmu-/elpis')

module.exports = (app) => class MyService extends BaseService {
  // 自动注入: this.app, this.config, this.curl
}

基类属性:

| 属性 | 说明 | |------|------| | this.app | Koa 应用实例 | | this.config | 合并后的配置对象 | | this.curl | superagent HTTP 客户端 |


buildApp(options)

前端生产构建(Webpack 一次性构建)。

const { buildApp } = require('@qinmu-/elpis')

await buildApp({
  pagesPath: '/path/to/business/app',
  watch: false,
})

参数:

| 参数 | 类型 | 默认值 | 说明 | |------|------|--------|------| | pagesPath | string | 框架自身 app/ | 业务项目的 app/ 目录路径 | | watch | boolean | false | 是否开启持续监听模式 |


startDevServer(options)

启动前端开发服务器(Express + webpack-dev-middleware + HMR)。

const { startDevServer } = require('@qinmu-/elpis')

startDevServer({
  pagesPath: '/path/to/business/app',
  port: 8081,
  koaOrigin: 'http://127.0.0.1:8080',
})

参数:

| 参数 | 类型 | 默认值 | 说明 | |------|------|--------|------| | pagesPath | string | 框架自身 app/ | 业务项目的 app/ 目录路径 | | port | number/string | 8081 | 开发服务器端口 | | koaOrigin | string | http://127.0.0.1:8080 | Koa 后端地址(用于 API 代理) |

功能:

  • 提供 HMR 热更新能力
  • /view/:page 路由渲染对应页面模板
  • /api/* 请求代理到 Koa 后端
  • / 重定向到 /view/page1

框架环境 API

通过 app.env 访问环境相关方法:

app.env.get()      // 获取当前环境: 'local' | 'beta' | 'production'
app.env.isLocal()  // 是否本地环境
app.env.isTest()   // 是否测试环境
app.env.isProd()   // 是否生产环境

常见问题

Q: 如何添加新的 API 接口?

按照以下步骤添加:

  1. 在 app/service/ 创建 Service 文件,编写业务逻辑
  2. 在 app/controller/ 创建 Controller 文件,调用 Service 并返回统一响应
  3. 在 app/router/ 创建路由文件,绑定 URL 到 Controller 方法
  4. (可选)在 app/router-schema/ 创建 Schema 文件,定义参数校验规则

Q: 如何添加新的前端页面?

  1. 在 app/pages/ 下创建 {pageName}/ 目录
  2. 创建 entry.{name}.js 入口文件,使用 boot() 启动
  3. 创建 Vue 组件文件
  4. 在 app/router/ 中添加页面渲染路由
  5. 重新构建前端或重启开发服务器

Q: 配置合并后,如何确认最终生效的配置?

在 Controller 或 Service 中打印 this.config 或 app.config 即可查看合并后的最终配置对象。

Q: 如何跳过 API 签名校验?

API 签名校验仅在 /api 路径下生效。如需临时关闭,可在 app/middleware.js 中注释掉 apiSignVerify 中间件注册。

Q: 前端请求如何自动携带项目标识?

前端 curl.js 会自动从 localStorage.projKey 读取当前项目标识,并在每个 API 请求头中注入 projKey。切换项目时需更新 localStorage.projKey。

Q: Model 模型层的 project 如何继承 model 配置?

Model 解析器会自动按 model/ 和 project/ 子目录分类加载,通过 projectExtendModel 实现 deep merge:project 中的配置覆盖 model 同名配置,model 中独有的配置保留。

Q: Webpack 构建报错 "Module not found"?

检查业务项目的 node_modules 是否安装了前端依赖。Webpack 的 resolveLoader.modules 配置会优先查找框架的 node_modules,但部分依赖可能需要业务项目自行安装。如遇缺失,在业务项目中安装对应包即可。

Q: 如何自定义 Webpack 配置?

目前 Webpack 配置封装在框架内部,业务项目通过 buildApp() 和 startDevServer() 的 pagesPath 参数指定自己的 app/ 目录路径。如需深度自定义 Webpack 配置,可在框架的 app/webpack/config/ 目录下修改对应的 webpack.base.js、webpack.dev.js 或 webpack.prod.js。


Elpis - 让全栈开发更简单