前言
最近学习实并现了一个全栈框架:elpis,这是一个什么框架,有什么用呢? --首先,这是一个中后台系统应用框架,主要技术栈是用vue3 + webpack5 + nodejs + koa + mysql, elpis 的核心设计理念是【领域模型】,现在市面上大部分的前端工作包括后端,基本都是CRUD的重复性工作,消耗了开发者大量的时间精力,从长远角度来看,单纯的CURD开发者一定会被淘汰。但elpis凭借它的设计思想可以沉淀80%的重复性工作,可以让开发者把更多的时间去用作系统框架的迭代开发。
项目结构
│
└── app # 应用源代码
│ │
│ ├── controller # controller处理器
│ ├── extend # 服务拓展
│ ├── middleware # 中间件
│ ├── pages # 页面目录
│ ├── public # 静态目录
│ ├── router # router接口路由分发
│ ├── router-schema # router路由规则
│ ├── service # service处理器
│ ├── view #
│ ├── webpack # webpack工程化配置
└── .editorconfig.js # 代码格式化文件
├── config # 配置文件
└── elips-core #
│ │
│ ├── loader # 静态文件 =》loader加载器 =》解析到运行时
├── model # 系统配置文件
├── test # 单元测试
├── .eslintignore # eslintignore文件
├── .eslintrc.js # eslint配置文件
├── .gitignore # gitignore文件
├── index.js
├── package.json # 项目运行环境及依赖包
├── README.md # 项目说明文件
系统设计
展示层
前端页面展示(CSR/SSR)、webpack工程化配置
BFF层
- BFF 分为接入层、业务层、服务层 接入层:定义路由规则 业务层:业务逻辑处理、环境配置、服务拓展等 服务层:servier处理器 =》连接数据库
数据层
操作数据库、调用外部服务处理数据,日志生成等
架构设计
=》一、基于nodejs实现服务端内核引擎 =》二、基于webpack5完成工程化建设 =》三、基于vue3完成领域模型架构建设 =》四、基于vue3完成动态组件库建设 =》五、完成框架 & npm包封装发布
一、基于nodejs实现服务端内核引擎
elpis-core:基于nodejs + koa实现loader解析器,读取解析位于app目录下对应的业务逻辑文件,实现页面渲染、API校验请求、数据库连接、服务启动等
const Koa = require("koa");
const path = require("path");
const { sep } = path; // 兼容不同操作系统上的斜杆
const env = require("./env");
const middlewareLoader = require("./loader/middleware");
const routerSchemaLoader = require("./loader/router-schema");
const routerLoader = require("./loader/router");
const controllerLoader = require("./loader/controller");
const serviceLoader = require("./loader/service");
const configLoader = require("./loader/config");
const extendLoader = require("./loader/extend");
/**
* 启动项目
* @params options 项目配置
* options = {
* name // 项目名称
* homePath // 项目首页
* }
*
*/
module.exports = {
start(options = {}) {
const app = new Koa();
// 应用配置
app.options = options;
// 基础路径
app.baseDir = process.cwd();
// 业务文件路径
app.businessPath = path.resolve(app.baseDir, `.${sep}app`);
// 初始化环境配置
app.env = env();
console.log(`-- [start] env: ${app.env.get()} --`);
// 加载 middleware
middlewareLoader(app);
console.log(`-- [start] load middleware done --`);
// 加载 routerSchema
routerSchemaLoader(app);
console.log(`-- [start] load routerSchema done --`);
// 加载 controller
controllerLoader(app);
// console.log(app.controller);
console.log(`-- [start] load controller done --`);
// 加载 service
serviceLoader(app);
// console.log(app.service);
console.log(`-- [start] load service done --`);
// 加载 config
configLoader(app);
// console.log(app.config);
console.log(`-- [start] load config done --`);
// 加载 extend
extendLoader(app);
// console.log(app);
console.log(`-- [start] load extend done --`);
// 注册 elips 全局中间件
const elipsMiddlewarePath = path.resolve(__dirname, `..${sep}app${sep}middleware.js`);
const elipsMiddleware = require(elipsMiddlewarePath);
elipsMiddleware(app);
console.log(`-- [start] load global elips middleware done --`);
// 注册业务全局中间件
try {
require(`${app.businessPath}${sep}middleware.js`)(app);
console.log(`-- [start] load global business middleware done --`);
} catch (e) {
console.log("[exception] there is no global business middleware file.");
}
// 注册路由 --- 需要放在最后
routerLoader(app);
console.log(`-- [start] load router done --`);
// 启动服务
try {
const port = process.env.PORT || 8080;
const host = process.env.IP || "0.0.0.0";
app.listen(port, host);
console.log(`Server running at:` + `http://${host}:${port}/`);
} catch (e) {
console.log("启动失败:", e);
}
return app;
},
};
二、基于webpack5完成工程化建设
webpack:处理业务文件,经过解析引擎(解析编译、模块分包、压缩优化),最终生成浏览器能识别的资源文件。 在这个项目里,首先会定义一个webpack-base配置,再根据不同环境处理合并,并生成最终对应的webpack配置。
webpack基础配置
{
entry: "", // 入口配置
module: {}, // 模块解析配置
outpup: {}, // 产物输出路径
resolve: {}, // 配置模块解析的具体行为(定义 webpack在打包时,如何找到并解析具体模块的路径)
plugins:[], // webpack插件
optimization: {}, // 配置打包输出优化(配置代码分割、模块合并、缓存、TreeShake,压缩等优化策略)
}
开发环境
开发服务器基于express实现,并引入webpack-dev-middleware(监控文件改动) 和 webpack-hot-middleware 中间件来实现开发环境热更新
生产环境
1.引入happypack实现多线程打包; 2.引入css-minimizer-webpack-plugin来优化并压缩 css 资源; 3.引入terser-webpack-plugin压缩代码,提升打包构建速度; 4.引入clean-webpack-plugin,实现构建前清空打包目录。
三、基于vue3完成领域模型架构建设
领域模型架构:通过一个模板配置(DSL),最终生成不同类型的页面 模板配置,结构如下:
const config = {
mode: 'dashboard', // 模板类型, 不同类型模板对应不同的模板数据结构
name: '', // 模板名称
title: '', // 模板标题
desc: '', // 模板描述
icon: '', // 模板图标
homePage: '', // 模板首页
// 头部菜单
menu: [
{
key: '', // 菜单唯一描述
name: '', // 菜单名称
menuType: '', // 菜单类型(菜单目录、菜单项) group | module
// 当 menuType === group
subMenu: [
{
// 可递归的菜单项 menuItem
},
// ...
],
// 当 menuType === module
moduleType: '', // 模块类型: sider | iframe | custom | schema
// 当 moduleType === schema
schemaConfig: {
api: '/api/user', // 数据源 restful api
schema: { // 板块数据配置
type: 'object',
properties: {
key: {
...schema, // 标准 schema 配置
type: 'string', // 字段类型
label: '', // 字段中文名
// 字段在 table 中的相关配置
tableOption: {
...elTableColumnConfig, // 标准 el-table-column 配置
toFixed: 0, // 数字字段,保留几位小数
visible: true, // 是否在表格中可见,默认true(false:不展示)
},
// 字段在 search-bar 对应的配置
searchOption: {
...elComponentConfig, // 标准 elementui 组件配置
comType: '', // 配置的组件类型
default: '', // 默认值
},
// 字段在动态组件中的配置,前缀对应componentConfig的键值
// 如:createFormOption = createForm + Option
createFormOption: {
...elComponentConfig,
comType: '', // 控件类型,如:input、select单独
visible: true, // 默认true,(true/false)false不展示
disabled: false, // 是否禁用(true/false)--是否可编辑
// comType === 'select' 时,启用
enumList: [], // 枚举值
// comType === 'dynamicSelect' 时,启用
api: '', // dynamicSelect 控件数据源
},
// 字段在 editForm 中的配置
editFormOption: {
...elComponentConfig,
comType: '', // 控件类型,如:input、select单独
visible: true, // 默认true,(true/false)false不展示
disabled: false, // 是否禁用(true/false)--是否可编辑
// comType === 'select' 时,启用
enumList: [], // 枚举值
// comType === 'dynamicSelect' 时,启用
api: '', // dynamicSelect 控件数据源
},
// 字段在 detailPanel 中的配置
detailPanelOption: {
...elComponentConfig,
}
},
// ...
},
// 必填 字段
required: [],
},
// 表格配置
tableConfig: {
// 表格头部按钮
headerButtons: [
{
label: '', // 按钮名称
eventKey: '', // 事件 key
// 按钮配置项
eventOption: {
// 当 eventKey === 'showComponent' 时,启用 comName,决定调用哪个组件
comName: '', // 组件名
},
...elButtonConfig, // 标准的el-button 配置项
},
// ...
],
// 表格行内按钮
rowButtons: [
{
label: '', // 按钮名称
eventKey: '', // 事件 key
eventOption: {
// 当 eventKey === 'showComponent' 时,启用 comName,决定调用哪个组件
comName: '', // 组件名
// 当 eventKey === 'remove'(add、remove、update、read)
params: {
// paramKey = 参数的键值
// rowValueKey = 参数值(格式:schema::tableKey时,从table中查找传值的值)
paramKey: rowValueKey,
}
}, // 按钮配置项
...elButtonConfig, // 标准的el-button 配置项
},
// ...
]
},
// search-bar 搜索配置
searchConfig: {},
// 动态组件配置
componentConfig: {
// 新增 form表单 组件配置
createForm: {
title: '', // 表单标题
saveBtnText: '', // 保存按钮文案
},
// 编辑 form 表单
editForm: {
mainKey: '', // 单条数据唯一标识-主键
title: '', // 组件标题
saveBtnText: '', // 保存按钮文案
},
// detail-panel 查看单条详情
detailPanel: {
mainKey: '', // 单条数据唯一标识-主键
title: '', // 组件标题
}
},
},
// 当 moduleType === custom
customConfig: {
path: '', // 自定义模块路径
},
// 当 moduleType === iframe
iframeConfig: {
path: '', // iframe 模块路径
},
// 当 moduleType === sider
siderConfig: {
menu: [
{
// 可递归的菜单项 menuItem ( moduleType !== sider)
},
// ...
],
}
}
]
}
module.exports = config;
在这份模板配置中,通过配置可以实现如iframe嵌入页面、自定义页面开发、但一个中后台系统最多的都是表格和查询组件,也就是配置中的schema类型。 基于这份模板配置,开发了顶部动态菜单、侧边栏菜单、element表格组件、条件查询组件,可以通过配置自动生成对应功能。
四、基于vue3完成动态组件库建设
在第三章节中的模板配置中,有涉及到动态组件的配置,比如条件查询组件中,有很多的子组件(input/select/dateRange/textarea等),那具体怎么实现呢? 1.首先基于element-plus,比如把el-input封装成我们自定义的组件,并通过v-bind实现el-input属性的透传,再通过配置文件引入
import input from "./input.vue";
const SearchItemConfig = {
input: {
component: input,
},
};
export default SearchItemConfig ;
2.通过动态引入input组件
<component :is="SearchItemConfig[配置模板中自定义的模板名]?.component" />
完成框架 & npm包封装发布
这样,一个轻量简易的全栈框架就搭建完成了,如果想要发布到npm上,可以参考下这篇文章: juejin.cn/spost/75528…