Elpis全栈项目总结
在本文的第一部分,我们将深入探讨 基于 Node.js 实现服务端内核引擎 的部分,并介绍整个项目的核心架构、技术选型以及如何构建一个高效、易维护的服务端引擎。通过分层架构、模块化设计,我们能够为前端提供更清晰、更高效的接口,并确保后端的可扩展性和性能。
一、基于 Node.js 实现服务端内核引擎
在构建一个现代化的后端应用时,Node.js 的高性能和灵活性使其成为了一个理想的选择。本部分将介绍如何利用 Node.js 和 Koa2 框架来实现一个服务端引擎,该引擎基于模块化和分层架构,支持高效的请求处理、日志记录和错误管理。
1. 项目架构与技术选型
本项目的架构主要分为 展示层、BFF 层(后端) 和 数据层,如下所示:
-
展示层:前端采用 Vue3 与 Element Plus,确保了页面的交互性和响应速度。
-
BFF 层(后端) :后端使用 Node.js 18 与 Koa2,提供高效、可扩展的服务端能力。Koa2 提供了更细粒度的控制,适用于需要高并发的系统。
-
数据层:使用 MySQL 数据库,适合需要强一致性和关系型数据的系统,同时通过 Log4js 进行日志管理。
这种架构设计保证了系统的高可用性与灵活性,同时也为后期的扩展提供了良好的基础。
2. 服务端框架搭建
我们使用 Koa2 作为 Web 框架,利用其中间件机制,按照 洋葱圈模型 处理请求。Koa2 提供了灵活的路由和中间件机制,可以轻松处理不同层级的逻辑。
核心代码:
javascript
复制编辑
const Koa = require('koa');
const app = new Koa();
app.listen(8080, () => {
console.log('Server running at http://localhost:8080');
});
通过上述代码,我们成功搭建了一个简单的 Koa2 服务端框架。在此基础上,我们添加了中间件来处理请求、响应和日志记录,确保系统能够处理高并发的请求。
中间件与路由处理
在服务端框架中,我们使用中间件进行日志记录、错误捕获等操作。比如,我们通过 koa-bodyparser
来解析请求体,通过 koa-router
来处理路由。
javascript
复制编辑
const router = require('koa-router')();
router.get('/api/project/list', projectController.getList);
这种中间件和路由的分离设计,使得代码的逻辑更加清晰,易于维护。
3. API 请求与 Controller 层
API 请求的核心流程包括路由定义、控制器处理和服务层调用。每次 API 请求都会经过以下几个步骤:
-
路由定义:首先,API 请求会被路由定义所匹配。
-
控制器处理:路由会调用相应的控制器方法进行业务逻辑处理。
-
服务层调用:控制器通过服务层处理更具体的任务(如数据库访问、外部服务调用等)。
示例:获取项目列表
-
路由定义:
javascript
复制编辑
module.exports = (app, router) => {
const { project: projectController } = app.controller;
router.get('/api/project/list', projectController.getList.bind(projectController));
};
-
Controller 层:
javascript
复制编辑
module.exports = (app) => {
return class ProjectController {
async getList(ctx) {
const { projectService } = app.service;
const res = await projectService.getList();
ctx.status = 200;
ctx.body = {
success: true,
data: res,
metadata: {}
};
}
};
};
-
Service 层:
javascript
复制编辑
module.exports = (app) => {
return class ProjectService {
async getList() {
return [
{ name: 'Project 1', desc: 'Description 1' },
{ name: 'Project 2', desc: 'Description 2' }
];
}
};
};
通过这种结构,后端逻辑被清晰地分层,避免了控制器和服务层之间的耦合,使得代码的可维护性和可扩展性大大增强。
4. 日志管理与错误处理
为了提升系统的可调试性和健壮性,我们通过 Log4js 进行日志记录,并在全局范围内捕获异常。日志管理和错误处理不仅有助于调试,还能确保在生产环境中的高可用性。
日志配置:
javascript
复制编辑
const log4js = require('log4js');
module.exports = (app) => {
let logger;
if (app.env.isLocal()) {
logger = console;
} else {
log4js.configure({
appenders: {
console: { type: 'console' },
dateFile: {
type: 'dateFile',
filename: './logs/application.log',
pattern: '.yyyy-MM-dd'
}
},
categories: { default: { appenders: ['console', 'dateFile'], level: 'trace' } }
});
logger = log4js.getLogger();
}
return logger;
};
错误处理:
javascript
复制编辑
module.exports = (app) => {
return async (ctx, next) => {
try {
await next();
} catch (err) {
app.logger.error('Error:', err);
ctx.status = 500;
ctx.body = { success: false, message: 'Internal Server Error' };
}
};
};
通过日志记录和错误处理,我们能够及时发现并修复系统中的潜在问题,保证系统的稳定性和可用性。
总结
通过以上的介绍,我们构建了一个基于 Node.js 和 Koa2 的服务端内核引擎,并通过分层架构将系统的各个部分模块化管理。这种设计不仅保证了代码的清晰与可维护性,还使得后端系统能够灵活地扩展和调整。接下来的部分,我们将继续深入探讨如何完善这个系统,确保其在实际生产环境中的高效运行。
二、Webpack5工程化
在这一部分,我们将深入探讨 Webpack5 的工程化设计。Webpack 是一个强大的 JavaScript 应用程序打包工具,它的核心功能不仅仅是打包 JavaScript 文件,还可以处理各种前端资源(如样式、图片、字体等),并通过插件和加载器提供了极大的灵活性。通过这一系列的设计与配置,我们能够在开发和生产环境中有效地优化前端构建流程。
1. Webpack5 项目架构与基本配置
首先,Webpack 作为前端构建工具,要求我们配置 entry、output、module 和 plugins 等内容。以下是 Webpack5 在一个典型项目中的基础配置:
项目入口与输出配置
Webpack 的 entry 配置决定了应用程序的入口点,而 output 配置则决定了最终产物的输出位置。
javascript
复制编辑
module.exports = {
// 入口文件配置
entry: './src/index.js',
// 输出配置
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
},
};
2. 解析与模块打包
不同的文件(如 .vue
, .js
, .scss
, .css
等)需要通过不同的解析引擎进行处理。Webpack 提供了强大的 loader 来实现这一点。例如,使用 babel-loader 来处理 ES6+ 的 JavaScript,使用 vue-loader 来解析 Vue 单文件组件。
配置 Module Rules:
javascript
复制编辑
module: {
rules: [
{
test: /.js$/,
use: 'babel-loader',
exclude: /node_modules/,
},
{
test: /.vue$/,
use: 'vue-loader',
},
{
test: /.css$/,
use: ['style-loader', 'css-loader'],
},
],
}
通过这些规则,Webpack 可以在打包时自动转换文件内容,保证代码在浏览器中的兼容性。
3. 模块分包与性能优化
一个典型的前端项目往往包含多个模块,如果我们将所有的代码打包成一个巨大的 JavaScript 文件,浏览器会面临性能瓶颈。为了解决这个问题,Webpack 提供了 代码分割(Code Splitting) 功能。
配置代码分割
通过 Webpack 的 splitChunks 配置,可以将重复使用的第三方库、公共模块和各个页面的代码拆分成多个文件,确保浏览器能够更高效地加载资源。
javascript
复制编辑
optimization: {
splitChunks: {
chunks: 'all',
maxAsyncRequests: 10,
maxInitialRequests: 10,
cacheGroups: {
vendor: {
test: /[\/]node_modules[\/]/,
name: 'vendor',
priority: 10,
},
},
},
}
这样,Webpack 会根据不同的规则将代码分成多个模块,使得浏览器能够更有效地缓存和加载文件。
4. 插件与自动化构建
除了基本的打包功能,Webpack 还提供了丰富的 插件,帮助我们进行自动化构建、优化输出、生成 HTML 模板等操作。
使用 HtmlWebpackPlugin 自动生成 HTML 文件
javascript
复制编辑
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html',
}),
],
};
通过 HtmlWebpackPlugin
,Webpack 会自动将打包后的 JavaScript 文件注入到生成的 HTML 文件中。
5. 开发与生产环境配置
Webpack 的配置通常会根据环境(开发环境和生产环境)进行调整。在开发环境中,我们通常需要启用 热模块替换(HMR)和 source-map 以便快速调试。而在生产环境中,我们需要压缩代码、优化资源,并配置更复杂的缓存策略。
开发环境配置(dev)
javascript
复制编辑
module.exports = {
mode: 'development',
devtool: 'eval-cheap-module-source-map',
devServer: {
contentBase: './dist',
hot: true,
open: true,
},
};
生产环境配置(prod)
javascript
复制编辑
module.exports = {
mode: 'production',
optimization: {
minimize: true,
splitChunks: {
chunks: 'all',
},
},
plugins: [
new TerserWebpackPlugin(),
],
};
开发环境主要专注于提升开发效率,而生产环境则侧重于代码压缩和优化。
6. 其他优化与扩展功能
除了基础的打包和压缩功能,Webpack 还支持通过 Happypack 实现多线程构建,利用 MiniCssExtractPlugin 将 CSS 单独提取为文件,进一步提升构建性能和页面加载速度。
MiniCssExtractPlugin 配置
javascript
复制编辑
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
plugins: [
new MiniCssExtractPlugin({
filename: 'styles/[name].[contenthash].css',
}),
],
};
总结
在本文中,我们探讨了如何使用 Webpack5 来实现一个现代化的前端构建系统。通过灵活的配置,Webpack 能够高效地处理不同类型的资源、优化构建过程、分离代码并进行性能优化。无论是在开发环境中实现热更新,还是在生产环境中进行代码压缩,Webpack5 都为前端工程化提供了强大的支持。这一切为后续的项目开发和维护打下了坚实的基础。
三、基于 Vue3 完成领域模型架构建设
在本文的第二部分,我们将探讨如何基于 Vue3 完成 领域模型架构 的建设。领域模型架构在大型项目中有着至关重要的作用,它帮助我们组织和管理业务逻辑,确保项目在功能扩展时保持清晰和可维护性。我们将结合 Vue3 和 Vue Router,以高效的方式实现项目的组织结构,并通过 Vuex 或 Pinia 管理全局状态和数据。
1. 领域模型设计
在项目的领域模型设计中,我们需要清晰地定义业务对象、逻辑处理和数据流。通常,领域模型的设计会根据项目的需求划分为不同的模块,每个模块拥有独立的逻辑和可复用的组件。例如,在电商系统中,我们可能会有 商品管理、订单管理 和 客户管理 等模块。
示例:电商系统的领域模型
javascript
复制编辑
module.exports = {
model: 'dashboard', // 模型类型
name: '电商系统', // 系统名称
menu: [
{
key: 'product',
name: '商品管理',
menuType: 'module',
moduleType: 'custom',
customConfig: {
path: '/todo'
}
},
{
key: 'order',
name: '订单管理',
menuType: 'module',
moduleType: 'iframe',
iframeConfig: {
path: 'http://www.baidu.com'
}
},
{
key: 'client',
name: '客户管理',
menuType: 'module',
moduleType: 'custom',
customConfig: {
path: '/todo'
}
}
]
};
在这个领域模型中,我们定义了一个包含商品管理、订单管理和客户管理的菜单结构,并且为每个菜单项指定了不同的显示方式(iframe
或 custom
)。这种结构化设计帮助我们清晰地分隔不同的功能模块。
2. DSL 设计与解析引擎
DSL(领域特定语言)是用来描述领域模型的特定语言。我们通过设计一个领域模型的 DSL,可以快速地描述和生成项目的配置。通常,这些配置数据会被送入解析引擎,并通过该引擎生成可执行的项目结构。
示例:DSL 设计
在电商系统的领域模型中,我们为不同的模块设置了特定的菜单、功能和配置路径。以下是一个简化的 DSL 配置:
javascript
复制编辑
{
model: 'dashboard', // 模板类型
name: '电商系统',
desc: '电商系统的管理后台',
homePage: '/dashboard',
menu: [
{
key: 'product',
name: '商品管理',
menuType: 'module',
moduleType: 'custom',
customConfig: {
path: '/todo'
}
},
{
key: 'order',
name: '订单管理',
menuType: 'module',
moduleType: 'iframe',
iframeConfig: {
path: 'http://www.baidu.com'
}
}
]
}
这种 DSL 设计通过简洁的配置,能够快速生成具体的项目结构和对应的页面路径。每个模块都可以通过不同的方式加载,提升了系统的灵活性和扩展性。
3. 实现 Vue3 领域模型架构
在 Vue3 项目中,领域模型的构建通常依赖于组件化开发和路由管理。每个模块都会被划分为不同的页面或组件,通过 Vue Router 来实现页面间的导航,通过 Pinia 或 Vuex 来管理状态。
示例:Vue3 组件化与路由配置
vue
复制编辑
<template>
<el-container class="dashboard-container">
<el-header>
<header-view :proj-name="projName" />
</el-header>
<el-main>
<router-view></router-view>
</el-main>
</el-container>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import headerView from './complex-view/header-view/header-view.vue';
import { useMenuStore } from '$store/menu.js';
const projName = ref('');
onMounted(() => {
projName.value = '电商系统';
});
</script>
<style scoped>
.dashboard-container {
height: 100%;
}
</style>
在这个组件中,我们通过 router-view
来动态渲染不同的页面,header-view
组件负责渲染头部菜单。通过 Pinia 或 Vuex 来管理全局状态,我们可以在不同的页面间共享数据。
4. 动态菜单与视图渲染
我们在 Vue3 项目中实现了动态菜单和视图渲染。通过 Vue Router 和 Vuex/Pinia,我们能够根据不同的领域模型动态加载菜单和视图,实现更加灵活的界面展示。
示例:动态加载视图
javascript
复制编辑
// vue-router 配置
const routes = [
{
path: '/product',
component: () => import('@/pages/ProductPage.vue'),
},
{
path: '/order',
component: () => import('@/pages/OrderPage.vue'),
}
];
通过这种方式,我们可以根据路由配置加载不同的页面,实现懒加载和按需加载,减少首屏加载的资源大小。
5. 领域模型与后台服务的集成
通过设计领域模型的 API,我们能够在后端与前端进行良好的数据交互。在后台实现领域模型的接口时,我们遵循 RESTful API 规范,确保每个请求都能够返回所需的数据结构。
示例:RESTful API 实现
javascript
复制编辑
module.exports = (app) => {
const BaseController = require('./base')(app);
return class ProjectController extends BaseController {
async getModelList(ctx) {
const { project: projectService } = app.service;
const modelList = await projectService.getModelList();
this.success(ctx, modelList);
}
};
};
在前端,使用 axios 或 fetch 进行接口调用,将从后端获取的领域模型数据渲染到页面中。
6. 实现 SchemaView 和 SchemaTable
在前端开发中,SchemaView 和 SchemaTable 是非常重要的组成部分,它们分别负责展示配置的业务数据和展示表格。通过动态的配置和 Vue3 的组件化方式,我们能够让这些组件具有很好的复用性和扩展性。
SchemaView 组件的实现
SchemaView 组件负责将业务模型中的配置解析并渲染到视图中。它基于从后端获取的 schemaConfig
和 tableConfig
动态生成相关的表单和表格视图。
vue
复制编辑
<template>
<el-row class="schema-view">
<search-panel />
<table-panel />
</el-row>
</template>
<script setup>
import { provide } from 'vue';
import SearchPanel from './complex-view/search-panel/search-panel.vue';
import TablePanel from './complex-view/table-panel/table-panel.vue';
import { useSchema } from './hook/schema.js';
const {
api,
tableSchema,
tableConfig
} = useSchema();
provide('schemaViewData', {
api,
tableSchema,
tableConfig
});
</script>
<style lang="less" scoped>
.schema-view {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
}
</style>
通过 useSchema
hook 获取业务模型的相关配置,我们将 api
, tableSchema
和 tableConfig
传递给子组件(如 SearchPanel
和 TablePanel
)。这些组件将利用这些配置动态生成页面内容。
7. 实现 SchemaSearchBar 组件
SchemaSearchBar 组件用于渲染业务模型中的搜索栏。它根据配置动态渲染各种输入组件(如 input
, select
, date-range
等),并提供 search
和 reset
操作。
vue
复制编辑
<template>
<el-form
v-if="schema && schema.properties"
:inline="true"
class="schema-search-bar"
@submit.prevent="search"
>
<el-form-item
v-for="(schemaItem,key) in schema?.properties"
:key="key"
:label="schemaItem.label"
>
<component
:is="SearchItemConfig[schemaItem.option?.comType]?.component"
:ref="handleSearchComList"
:schema-key="key"
:schema="schemaItem"
@loaded="handleChildLoaded"
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
plain
class="search-btn"
@click="search"
>
搜索
</el-button>
<el-button
type="default"
plain
class="reset-btn"
@click="reset"
>
重置
</el-button>
</el-form-item>
</el-form>
</template>
<script setup>
import { ref, toRefs } from 'vue';
import SearchItemConfig from './search-item-config.js';
const props = defineProps({
schema: Object,
});
const { schema } = toRefs(props);
const emit = defineEmits(['load', 'search', 'reset']);
const searchComList = ref([]);
const handleSearchComList = (el) => {
searchComList.value.push(el);
};
const getValue = () => {
let dtoObj = {};
searchComList.value.forEach((component) => {
dtoObj = { ...dtoObj, ...component?.getValue() };
});
return dtoObj;
};
let childComLoadedCount = 0;
const handleChildLoaded = () => {
childComLoadedCount++;
if (childComLoadedCount >= Object.keys(schema?.value?.properties).length) {
emit('load', getValue());
}
};
const search = () => {
emit('search', getValue());
};
const reset = () => {
searchComList.value.forEach((component) => component?.reset());
emit('reset');
};
</script>
<style lang="less">
.schema-search-bar {
min-width: 500px;
}
</style>
这个组件通过动态渲染不同的输入框组件来适应不同的搜索需求。所有的表单项(input
, select
, dynamicSelect
, dateRange
)都可以通过 SchemaSearchBar 的配置项传递进来。
8. 实现 SchemaTable 组件
SchemaTable 组件负责展示业务模型的表格数据,并提供一些操作按钮(如 修改
, 删除
)。它根据传入的 tableConfig
配置动态渲染表格和操作按钮。
vue
复制编辑
<template>
<div class="schema-table">
<el-table
v-if="schema && schema.properties"
v-loading="loading"
:data="tableData"
class="table"
>
<template v-for="(schemaItem, key) in schema.properties">
<el-table-column
v-if="schemaItem.option.visiable !== false"
:key="key"
:prop="key"
:label="schemaItem.label"
v-bind="schemaItem.option"
/>
</template>
<el-table-column
v-if="buttons?.length > 0"
label="操作"
fixed="right"
:width="operationWidth"
>
<template #default="scope">
<el-button
v-for="item in buttons"
link
v-bind="item"
@click="operationHandler({ btnConfig: item, rowData: scope.row })"
>
{{ item.label }}
</el-button>
</template>
</el-table-column>
</el-table>
<el-row class="pagination" justify="end">
<el-pagination
:current-page="currentPage"
:page-size="pageSize"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="onPageSizeChange"
@current-change="onCurrentPageChange"
/>
</el-row>
</div>
</template>
<script setup>
import { ref, toRefs, computed, watch, nextTick, onMounted } from 'vue';
import $curl from '$common/curl.js';
const props = defineProps({
schema: Object,
api: String,
buttons: Array,
});
const { schema, api, buttons } = toRefs(props);
const emit = defineEmits(['operate']);
const operationWidth = computed(() => {
return buttons?.value?.length > 0
? buttons.value.reduce((pre, cur) => pre + cur.label.length * 18, 50)
: 50;
});
const loading = ref(false);
const tableData = ref([]);
const currentPage = ref(1);
const pageSize = ref(50);
const total = ref(0);
onMounted(() => {
initData();
});
watch([schema, api], () => {
initData();
}, { deep: true });
const initData = () => {
currentPage.value = 1;
pageSize.value = 50;
nextTick(async () => {
await loadTableData();
});
};
const loadTableData = async () => {
if (!api.value) return;
showLoading();
const res = await $curl({
method: 'get',
url: `${api.value}/list`,
query: {
page: currentPage.value,
size: pageSize.value,
},
});
hideLoading();
if (!res || !res.success || !Array.isArray(res.data)) {
tableData.value = [];
total.value = 0;
return;
}
tableData.value = res.data;
total.value = res.metadata.total;
};
const showLoading = () => {
loading.value = true;
};
const hideLoading = () => {
loading.value = false;
};
const operationHandler = ({ btnConfig, rowData }) => {
emit('operate', { btnConfig, rowData });
};
const onPageSizeChange = async (value) => {
pageSize.value = value;
await loadTableData();
};
const onCurrentPageChange = async (value) => {
currentPage.value = value;
await loadTableData();
};
defineExpose({
initData,
loadTableData,
showLoading,
hideLoading,
});
</script>
<style scoped>
.schema-table {
flex: 1;
display: flex;
flex-direction: column;
.table {
flex: 1;
}
.pagination {
margin: 10px 0;
}
}
</style>
通过这种方式,我们能够动态地从后端获取表格数据,并根据模型配置来展示对应的列和操作按钮。用户点击按钮后,operationHandler
方法会触发相应的操作。
9. 领域模型的接口和数据交互
每个领域模型的接口都可以在 Vue3 中通过 axios 或 fetch 进行封装,实现与后端的交互。通过动态的 API 请求,我们可以实现业务模型的 增、删、改、查 功能,并通过 SchemaView 和 SchemaTable 组件渲染数据。
示例:后端接口调用
javascript
复制编辑
const getProductList = async () => {
const res = await $curl({
method: 'get',
url: '/api/proj/product/list',
query: {
page: 1,
size: 50,
},
});
return res;
};
通过这种方式,我们能够保证前端和后端的数据交互高效且简洁。
总结
在本部分中,我们探讨了如何基于 Vue3 完成领域模型架构建设。从 DSL 设计 到 Vue3 组件化,再到 路由和视图渲染的动态加载,我们展示了如何构建一个灵活、高效且可维护的前端系统。通过与后台服务的良好集成,我们能够实现完整的前后端分离架构,提升开发效率并优化用户体验
四、基于 Vue3 完成动态组件库建设
在本文的第四部分,我们将深入探讨如何基于 Vue3 构建一个动态组件库。通过这一组件库,我们可以灵活地为不同的业务需求创建可复用的组件,这些组件能够根据配置动态生成表单、面板、表格等,极大地提高开发效率和代码的复用性。
1. 动态组件库的需求分析
在企业级应用中,我们经常需要处理表单、表格、面板等 UI 组件的展示和交互。传统的做法是为每个功能模块编写独立的组件,但随着项目复杂度的增加,这种做法容易导致重复代码和低效率。因此,我们需要一个动态组件库,它能够根据不同的配置和需求,动态生成相应的 UI 组件。
关键需求:
-
动态生成表单:根据业务模型和配置,动态生成表单(如新增、编辑、详情表单)。
-
动态生成面板:通过配置展示详情面板,显示业务对象的详细信息。
-
复用性和扩展性:组件库应具备良好的复用性,可以通过不同的配置生成不同的组件实例。
2. 动态表单组件实现
SchemaForm 组件是我们动态组件库的核心,它根据传入的 schema 配置生成表单项,并提供表单校验、数据收集等功能。我们将通过 SchemaForm 结合 CreateForm 和 EditForm,实现表单的创建与编辑功能。
示例:SchemaForm 组件实现
vue
复制编辑
<template>
<el-row class="schema-form">
<template v-for="(item, key) in schema.properties">
<component
:is="FormItemConfig[item.option.comType]?.component"
:key="key"
:schema="item"
:model="model[key]"
:schema-key="key"
/>
</template>
</el-row>
</template>
<script setup>
import { ref } from 'vue';
import FormItemConfig from './form-item-config';
const props = defineProps({
schema: Object,
model: Object
});
const formComList = ref([]);
const validate = () => {
return formComList.value.every(component => component.validate());
};
const getValue = () => {
return formComList.value.reduce((dtoObj, component) => {
return { ...dtoObj, ...component.getValue() };
}, {});
};
defineExpose({
validate,
getValue,
});
</script>
<style scoped>
.schema-form {
display: flex;
flex-wrap: wrap;
}
</style>
-
动态表单生成:根据
schema
配置动态渲染表单项。
-
表单验证和数据收集:
validate
方法校验表单数据,getValue
方法收集表单数据。
3. 编辑表单与详情面板实现
EditForm 和 DetailPanel 组件是基于 SchemaForm 的两个衍生组件,分别用于表单的编辑和详情数据的展示。
示例:EditForm 组件实现
vue
复制编辑
<template>
<el-drawer v-model="isShow" direction="rtl" :size="550">
<template #header>
<h3>{{ title }}</h3>
</template>
<template #default>
<schema-form
ref="schemaFormRef"
v-loading="loading"
:schema="components[name].schema"
:model="dtoModel"
/>
</template>
<template #footer>
<el-button type="primary" @click="save">{{ saveBtnText }}</el-button>
</template>
</el-drawer>
</template>
<script setup>
import { ref, inject } from 'vue';
import SchemaForm from '$widgets/schema-form/schema-form.vue';
const name = ref('editForm');
const schemaFormRef = ref(null);
const isShow = ref(false);
const loading = ref(false);
const title = ref('');
const saveBtnText = ref('');
const dtoModel = ref({});
const { api, components } = inject('schemaViewData');
const emit = defineEmits(['command']);
const show = (rowData) => {
const { config } = components.value[name.value];
title.value = config.title;
saveBtnText.value = config.saveBtnText;
dtoModel.value = {};
isShow.value = true;
fetchFormData();
};
const fetchFormData = async () => {
if (loading.value) return;
loading.value = true;
const res = await fetchData(api.value, { product_id: rowData.product_id });
loading.value = false;
if (res) dtoModel.value = res.data;
};
const save = async () => {
if (loading.value) return;
if (!schemaFormRef.value.validate()) return;
loading.value = true;
const res = await fetchData(api.value, { ...dtoModel.value });
loading.value = false;
if (res) {
emit('command', { event: 'loadTableData' });
}
};
defineExpose({
name,
show
});
</script>
-
动态渲染与编辑:根据
schema
动态生成表单,并通过 dtoModel
传递数据。
-
保存与回显:通过
fetchFormData
获取数据并回显,通过 save
方法保存编辑数据。
4. 动态面板组件实现
DetailPanel 组件用于展示详情数据,通常通过接口获取数据并在面板中显示。
示例:DetailPanel 组件实现
vue
复制编辑
<template>
<el-drawer v-model="isShow" direction="rtl" :size="550">
<template #header>
<h3>{{ title }}</h3>
</template>
<template #default>
<el-card v-loading="loading" class="detail-panel">
<el-row v-for="(item, key) in components[name].schema.properties" :key="key">
<el-row class="item-label">{{ item.label }}:</el-row>
<el-row class="item-value">{{ dtoModel[key] }}</el-row>
</el-row>
</el-card>
</template>
</el-drawer>
</template>
<script setup>
import { ref, inject } from 'vue';
const isShow = ref(false);
const loading = ref(false);
const title = ref('');
const dtoModel = ref({});
const { api, components } = inject('schemaViewData');
const name = ref('detailPanel');
const show = (rowData) => {
const { config } = components.value[name.value];
title.value = config.title;
dtoModel.value = {};
isShow.value = true;
fetchFormData(rowData);
};
const fetchFormData = async (rowData) => {
loading.value = true;
const res = await fetchData(api.value, { product_id: rowData.product_id });
loading.value = false;
if (res) dtoModel.value = res.data;
};
defineExpose({
name,
show
});
</script>
-
数据展示:通过
dtoModel
展示详情数据。
-
动态获取数据:根据主键
product_id
请求详情数据并在面板中展示。
5. 组件库的扩展性和复用
通过以上组件的实现,我们的动态组件库可以方便地扩展和复用。每个组件(如 SchemaForm
, EditForm
, DetailPanel
)都基于 schema 配置动态生成,能够根据不同的业务需求生成对应的表单和面板。此外,组件的组合(如 createForm
、editForm
)可以通过配置轻松地在多个地方复用。
通过这种方式,我们能够大幅度提高开发效率,避免重复造轮子,同时保证项目的可维护性和扩展性。
总结
在本部分中,我们介绍了如何基于 Vue3 构建一个动态组件库,包括 动态表单、编辑表单 和 详情面板 的实现。这些组件基于 schema 配置动态生成,提供了强大的灵活性和扩展性,适用于各种不同的业务需求。通过这种组件化和配置化的方式,我们可以大大提高开发效率,并保持代码的高复用性。
五、完成框架 NPM 包抽离封装并发布
在本文的第五部分,我们将详细介绍如何将 Elpis 框架的核心逻辑进行抽离并封装为一个 NPM 包,然后将其发布到 NPM 上。这样,其他项目可以直接通过 npm install @aodi/elpis
来使用这个框架,并能在自己的项目中进行集成与扩展。
1. 抽离核心代码并封装为 NPM 包
首先,我们需要将 Elpis 框架的核心代码抽离出来,使其能够作为一个独立的模块进行使用。为了实现这一点,我们将 Elpis 中的业务逻辑、服务端的框架部分以及相关的配置文件抽离到一个单独的包中。
步骤:
- 在
elpis
项目中,将相关的文件移动到独立的目录下,确保核心代码与业务逻辑分离。例如,将 elpis-core
放入一个单独的文件夹。
- 修改
package.json
文件,为包提供名称、版本号和依赖信息。需要将框架的相关逻辑暴露出来,使其他项目能够通过 NPM 安装并使用。
json
复制编辑
{
"name": "@aodi/elpis", // 采用 NPM 组织 + 项目命名的格式
"version": "1.0.0",
"main": "index.js",
"scripts": {
"build": "node --max_old_space_size=4096 ./app/webpack/prod.js"
},
"dependencies": {
"koa": "^2.7.0",
"vue": "^3.3.4",
"vuex": "^4.1.0"
},
"devDependencies": {
"webpack": "^5.88.1",
"vue-loader": "^17.2.2",
"babel-loader": "^8.0.4"
}
}
- 通过运行
npm login
登录到 NPM,准备发布包。
bash
复制编辑
npm login
- 然后,执行
npm publish
命令将包发布到 NPM 仓库中:
bash
复制编辑
npm publish
2. 在 elpis-demo
项目中集成并使用 NPM 包
一旦 Elpis 被成功发布为 NPM 包,其他项目就可以通过 npm install @aodi/elpis
命令安装并使用它。
步骤:
- 在
elpis-demo
项目中,首先通过 NPM 安装 Elpis:
bash
复制编辑
npm install @aodi/elpis
- 在
elpis-demo
的代码中,直接引入并使用 Elpis 提供的功能模块:
javascript
复制编辑
const { serverStart } = require('@aodi/elpis');
// 启动服务
const app = serverStart({
name: 'ElpisDemo',
homePage: '/view/dashboard'
});
这样,elpis-demo
项目就可以直接通过引入 Elpis 核心包来实现框架的启动与服务的管理。
3. 配置 Webpack 打包与 NPM 包依赖问题
在 elpis-demo
中使用 Elpis 时,有时需要修改 Webpack 配置或者手动安装某些依赖,特别是当 Elpis 的某些依赖没有安装时。为了确保 elpis-demo
项目能够顺利运行,以下是必要的步骤:
修改 Webpack 配置:
- 在
elpis-demo
中,确保 webpack.config.js
中设置了正确的别名,以便能够正确加载 elpis-demo 自定义的别名:
javascript
复制编辑
module.exports = {
resolve: {
alias: {
'$demo': 'demo'
}
}
};
4. 在 elpis-demo
中配置并发布
为了使 elpis-demo
项目更容易使用 Elpis,你可以通过在 elpis-demo
的 package.json
中添加一些启动脚本,并配置不同的环境(如开发、生产等)来使得包管理更加灵活。
json
复制编辑
{
"scripts": {
"dev": "set _ENV=local&& nodemon ./server.js",
"prod": "set _ENV=production&& node ./server.js",
"build:dev": "set _ENV=local&& node --max_old_space_size=4096 ./build.js"
}
}
5. 总结
通过将 Elpis 核心逻辑抽离为一个 NPM 包,我们能够将其发布到 NPM 上,其他开发者和团队可以方便地通过 npm install @aodi/elpis
来使用这个框架。这样,不仅减少了重复造轮子的工作,还能方便团队之间共享和管理代码。通过简单的配置,Elpis 可以集成到不同的项目中,实现服务端与前端的有效协作。
六、框架应用与项目实践
在本文的第六部分,我们将探讨如何应用 Elpis 框架并在实际项目中进行开发和部署。通过人员管理模块、登录校验和登出功能的实现,结合持续集成(CI)和持续部署(CD)等工具的应用,我们可以构建一个完整的业务系统。
1. 人员管理模块实践
在 Elpis 框架中,人员管理模块是最常见的模块之一。我们通过 schema 和 API 配置,结合 Elpis 提供的自动化组件,快速实现人员管理系统的增删改查(CRUD)功能。
人员管理模块的领域模型
首先,定义了一个简单的 人员管理模块。在 elpis-demo/model/people/model.js
文件中,我们为人员管理系统配置了模型。
javascript
复制编辑
module.exports = {
module: 'people',
name: '人员管理系统',
menu: [{
key: 'user',
name: '人员管理',
menuType: 'module',
moduleType: 'schema',
schemaConfig: {
api: '/api/proj/user',
schema: {
type: 'object',
properties: {
user_id: { type: 'string', label: '用户ID' },
username: { type: 'string', label: '账号' },
nickname: { type: 'string', label: '昵称' },
sex: {
type: 'number',
label: '性别',
searchOption: { comType: 'select', enumList: [{ label: '男', value: 1 }, { label: '女', value: 2 }] }
},
create_time: { type: 'string', label: '创建时间', searchOption: { comType: 'dateRange' } }
}
},
required: ['username', 'nickname', 'sex']
},
tableConfig: {
headerButtons: [{
label: '新增用户',
eventKey: 'showComponent',
eventOption: { comName: 'createForm' },
type: 'primary'
}],
rowButtons: [{
label: '查看详情',
eventKey: 'showComponent',
eventOption: { comName: 'detailPanel' },
type: 'primary'
}, {
label: '修改',
eventKey: 'showComponent',
eventOption: { comName: 'editForm' },
type: 'warning'
}, {
label: '删除',
eventKey: 'remove',
eventOption: { params: { user_id: 'schema::user_id' } },
type: 'danger'
}]
},
componentConfig: {
createForm: { title: '新增用户', saveBtnText: '保存' },
editForm: { title: '修改用户', saveBtnText: '保存' },
detailPanel: { title: '用户详情' }
}
}]
}
API 接口配置
在 elpis-demo/app/router-schema/user.js
文件中,我们定义了 RESTful API 接口,规范了用户的增删改查操作。
javascript
复制编辑
module.exports = {
'/api/proj/user/list': {
get: { query: { page: { type: 'string' }, size: { type: 'string' } } }
},
'/api/proj/user': {
post: {
body: {
type: 'object',
properties: {
username: { type: 'string' },
nickname: { type: 'string' },
sex: { type: 'number' },
desc: { type: 'string' }
}
}
},
put: { body: { properties: { user_id: { type: 'string' }, nickname: { type: 'string' }, sex: { type: 'number' } } } },
delete: { body: { properties: { user_id: { type: 'string' } } } },
get: { query: { properties: { user_id: { type: 'string' } } } }
}
}
2. 登录与登出功能
为了增强系统的安全性和用户管理,Elpis 框架集成了登录与登出功能。登录功能使用 JWT 生成令牌,而登出功能则是通过清除令牌并重定向用户到登录页面来实现。
登录功能
在 elpis-demo/app/pages/auth/complex-view/login/login.vue
中实现了简单的登录表单,通过 axios 向后端发送请求。
vue
复制编辑
<template>
<el-row v-loading="loading">
<el-input v-model="username" placeholder="请输入账号" class="username"></el-input>
<el-input v-model="password" placeholder="请输入密码" class="password" show-password></el-input>
<el-button type="primary" @click="login" class="login-btn">登录</el-button>
</el-row>
</template>
<script setup>
import { ref } from 'vue';
import { ElMessage } from 'element-plus';
import $curl from '$elpisCurl';
const loading = ref(false);
const username = ref('admin');
const password = ref('123456');
const login = async () => {
loading.value = true;
const res = await $curl({
method: 'post',
url: '/api/auth/login',
data: { username: username.value, password: password.value }
});
loading.value = false;
if (!res || !res.success) return;
ElMessage.success('登录成功');
localStorage.setItem('nickname', res?.data?.nickname);
window.location = '/view/project-list';
};
</script>
登出功能
登出功能的实现通过删除 JWT 令牌并重定向到登录页面。
vue
复制编辑
<template>
<el-dropdown @command="handleUserCommand">
<span class="username">{{ userName }}</span> <i class="el-icon-arrow-down el-icon--right" />
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script setup>
import { ref } from 'vue';
const userName = ref(localStorage.getItem('nickname') || '管理员');
const handleUserCommand = function (event) {
if (event === 'logout') {
window.location = '/api/auth/logout';
}
};
</script>
3. 持续集成与持续部署(CI/CD)
持续集成(CI)
在 Elpis 的开发过程中,持续集成(CI)是非常重要的,它确保每次代码提交后都能进行自动化构建、测试和推送。通过 Jenkins 等工具,我们可以设置自动化流水线来构建和测试代码。
shell
复制编辑
npm install --production
npm run build:prod
持续部署(CD)
在 CD 流程中,我们将代码部署到生产环境。通过 Docker 和 Kubernetes,我们能够实现自动化的部署和扩展,确保服务的高可用性和自动化管理。
yaml
复制编辑
apiVersion: apps/v1
kind: Deployment
metadata:
name: elpis-demo-deployment
spec:
replicas: 1
selector:
matchLabels:
app: elpis-demo
template:
metadata:
labels:
app: elpis-demo
spec:
containers:
- image: 'your-docker-image'
name: elpis-demo-container
ports:
- containerPort: 8081
4. 总结
通过 Elpis 框架,我们可以实现快速的项目开发和部署。通过 人员管理模块 的实现,我们能够在项目中轻松实现业务逻辑。结合 持续集成(CI) 和 持续部署(CD) ,我们能够高效地将代码推送到生产环境,确保项目的稳定性和可扩展性。