Elpis全栈项目总结

Elpis全栈项目总结

在本文的第一部分,我们将深入探讨 基于 Node.js 实现服务端内核引擎 的部分,并介绍整个项目的核心架构、技术选型以及如何构建一个高效、易维护的服务端引擎。通过分层架构、模块化设计,我们能够为前端提供更清晰、更高效的接口,并确保后端的可扩展性和性能。


一、基于 Node.js 实现服务端内核引擎

在构建一个现代化的后端应用时,Node.js 的高性能和灵活性使其成为了一个理想的选择。本部分将介绍如何利用 Node.js 和 Koa2 框架来实现一个服务端引擎,该引擎基于模块化和分层架构,支持高效的请求处理、日志记录和错误管理。

1. 项目架构与技术选型

本项目的架构主要分为 展示层BFF 层(后端)数据层,如下所示:

  • 展示层 :前端采用 Vue3Element Plus,确保了页面的交互性和响应速度。
  • BFF 层(后端) :后端使用 Node.js 18Koa2,提供高效、可扩展的服务端能力。Koa2 提供了更细粒度的控制,适用于需要高并发的系统。
  • 数据层 :使用 MySQL 数据库,适合需要强一致性和关系型数据的系统,同时通过 Log4js 进行日志管理。

这种架构设计保证了系统的高可用性与灵活性,同时也为后期的扩展提供了良好的基础。


2. 服务端框架搭建

我们使用 Koa2 作为 Web 框架,利用其中间件机制,按照 洋葱圈模型 处理请求。Koa2 提供了灵活的路由和中间件机制,可以轻松处理不同层级的逻辑。

核心代码:
ini 复制代码
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 来处理路由。

ini 复制代码
javascript
复制编辑
const router = require('koa-router')();
router.get('/api/project/list', projectController.getList);

这种中间件和路由的分离设计,使得代码的逻辑更加清晰,易于维护。


3. API 请求与 Controller 层

API 请求的核心流程包括路由定义、控制器处理和服务层调用。每次 API 请求都会经过以下几个步骤:

  1. 路由定义:首先,API 请求会被路由定义所匹配。
  2. 控制器处理:路由会调用相应的控制器方法进行业务逻辑处理。
  3. 服务层调用:控制器通过服务层处理更具体的任务(如数据库访问、外部服务调用等)。
示例:获取项目列表
  1. 路由定义
ini 复制代码
javascript
复制编辑
module.exports = (app, router) => {
    const { project: projectController } = app.controller;
    router.get('/api/project/list', projectController.getList.bind(projectController));
};
  1. Controller 层
ini 复制代码
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: {}
            };
        }
    };
};
  1. Service 层
javascript 复制代码
javascript
复制编辑
module.exports = (app) => {
    return class ProjectService {
        async getList() {
            return [
                { name: 'Project 1', desc: 'Description 1' },
                { name: 'Project 2', desc: 'Description 2' }
            ];
        }
    };
};

通过这种结构,后端逻辑被清晰地分层,避免了控制器和服务层之间的耦合,使得代码的可维护性和可扩展性大大增强。


4. 日志管理与错误处理

为了提升系统的可调试性和健壮性,我们通过 Log4js 进行日志记录,并在全局范围内捕获异常。日志管理和错误处理不仅有助于调试,还能确保在生产环境中的高可用性。

日志配置:
php 复制代码
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;
};
错误处理:
ini 复制代码
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.jsKoa2 的服务端内核引擎,并通过分层架构将系统的各个部分模块化管理。这种设计不仅保证了代码的清晰与可维护性,还使得后端系统能够灵活地扩展和调整。接下来的部分,我们将继续深入探讨如何完善这个系统,确保其在实际生产环境中的高效运行。

二、Webpack5工程化

在这一部分,我们将深入探讨 Webpack5 的工程化设计。Webpack 是一个强大的 JavaScript 应用程序打包工具,它的核心功能不仅仅是打包 JavaScript 文件,还可以处理各种前端资源(如样式、图片、字体等),并通过插件和加载器提供了极大的灵活性。通过这一系列的设计与配置,我们能够在开发和生产环境中有效地优化前端构建流程。


1. Webpack5 项目架构与基本配置

首先,Webpack 作为前端构建工具,要求我们配置 entryoutputmoduleplugins 等内容。以下是 Webpack5 在一个典型项目中的基础配置:

项目入口与输出配置

Webpack 的 entry 配置决定了应用程序的入口点,而 output 配置则决定了最终产物的输出位置。

java 复制代码
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 复制代码
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 配置,可以将重复使用的第三方库、公共模块和各个页面的代码拆分成多个文件,确保浏览器能够更高效地加载资源。

yaml 复制代码
javascript
复制编辑
optimization: {
  splitChunks: {
    chunks: 'all',
    maxAsyncRequests: 10,
    maxInitialRequests: 10,
    cacheGroups: {
      vendor: {
        test: /[\/]node_modules[\/]/,
        name: 'vendor',
        priority: 10,
      },
    },
  },
}

这样,Webpack 会根据不同的规则将代码分成多个模块,使得浏览器能够更有效地缓存和加载文件。

4. 插件与自动化构建

除了基本的打包功能,Webpack 还提供了丰富的 插件,帮助我们进行自动化构建、优化输出、生成 HTML 模板等操作。

使用 HtmlWebpackPlugin 自动生成 HTML 文件
ini 复制代码
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)
yaml 复制代码
javascript
复制编辑
module.exports = {
  mode: 'development',
  devtool: 'eval-cheap-module-source-map',
  devServer: {
    contentBase: './dist',
    hot: true,
    open: true,
  },
};
生产环境配置(prod)
css 复制代码
javascript
复制编辑
module.exports = {
  mode: 'production',
  optimization: {
    minimize: true,
    splitChunks: {
      chunks: 'all',
    },
  },
  plugins: [
    new TerserWebpackPlugin(),
  ],
};

开发环境主要专注于提升开发效率,而生产环境则侧重于代码压缩和优化。


6. 其他优化与扩展功能

除了基础的打包和压缩功能,Webpack 还支持通过 Happypack 实现多线程构建,利用 MiniCssExtractPlugin 将 CSS 单独提取为文件,进一步提升构建性能和页面加载速度。

MiniCssExtractPlugin 配置
ini 复制代码
javascript
复制编辑
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'styles/[name].[contenthash].css',
    }),
  ],
};

总结

在本文中,我们探讨了如何使用 Webpack5 来实现一个现代化的前端构建系统。通过灵活的配置,Webpack 能够高效地处理不同类型的资源、优化构建过程、分离代码并进行性能优化。无论是在开发环境中实现热更新,还是在生产环境中进行代码压缩,Webpack5 都为前端工程化提供了强大的支持。这一切为后续的项目开发和维护打下了坚实的基础。

三、基于 Vue3 完成领域模型架构建设

在本文的第二部分,我们将探讨如何基于 Vue3 完成 领域模型架构 的建设。领域模型架构在大型项目中有着至关重要的作用,它帮助我们组织和管理业务逻辑,确保项目在功能扩展时保持清晰和可维护性。我们将结合 Vue3Vue Router ,以高效的方式实现项目的组织结构,并通过 VuexPinia 管理全局状态和数据。


1. 领域模型设计

在项目的领域模型设计中,我们需要清晰地定义业务对象、逻辑处理和数据流。通常,领域模型的设计会根据项目的需求划分为不同的模块,每个模块拥有独立的逻辑和可复用的组件。例如,在电商系统中,我们可能会有 商品管理订单管理客户管理 等模块。

示例:电商系统的领域模型
css 复制代码
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'
            }
        }
    ]
};

在这个领域模型中,我们定义了一个包含商品管理、订单管理和客户管理的菜单结构,并且为每个菜单项指定了不同的显示方式(iframecustom)。这种结构化设计帮助我们清晰地分隔不同的功能模块。


2. DSL 设计与解析引擎

DSL(领域特定语言)是用来描述领域模型的特定语言。我们通过设计一个领域模型的 DSL,可以快速地描述和生成项目的配置。通常,这些配置数据会被送入解析引擎,并通过该引擎生成可执行的项目结构。

示例:DSL 设计

在电商系统的领域模型中,我们为不同的模块设置了特定的菜单、功能和配置路径。以下是一个简化的 DSL 配置:

css 复制代码
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 来实现页面间的导航,通过 PiniaVuex 来管理状态。

示例:Vue3 组件化与路由配置
xml 复制代码
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 组件负责渲染头部菜单。通过 PiniaVuex 来管理全局状态,我们可以在不同的页面间共享数据。


4. 动态菜单与视图渲染

我们在 Vue3 项目中实现了动态菜单和视图渲染。通过 Vue RouterVuex/Pinia,我们能够根据不同的领域模型动态加载菜单和视图,实现更加灵活的界面展示。

示例:动态加载视图
javascript 复制代码
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 复制代码
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);
    }
  };
};

在前端,使用 axiosfetch 进行接口调用,将从后端获取的领域模型数据渲染到页面中。


6. 实现 SchemaView 和 SchemaTable

在前端开发中,SchemaViewSchemaTable 是非常重要的组成部分,它们分别负责展示配置的业务数据和展示表格。通过动态的配置和 Vue3 的组件化方式,我们能够让这些组件具有很好的复用性和扩展性。

SchemaView 组件的实现

SchemaView 组件负责将业务模型中的配置解析并渲染到视图中。它基于从后端获取的 schemaConfigtableConfig 动态生成相关的表单和表格视图。

xml 复制代码
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, tableSchematableConfig 传递给子组件(如 SearchPanelTablePanel)。这些组件将利用这些配置动态生成页面内容。


7. 实现 SchemaSearchBar 组件

SchemaSearchBar 组件用于渲染业务模型中的搜索栏。它根据配置动态渲染各种输入组件(如 input, select, date-range 等),并提供 searchreset 操作。

ini 复制代码
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 配置动态渲染表格和操作按钮。

ini 复制代码
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 中通过 axiosfetch 进行封装,实现与后端的交互。通过动态的 API 请求,我们可以实现业务模型的 增、删、改、查 功能,并通过 SchemaViewSchemaTable 组件渲染数据。

示例:后端接口调用
ini 复制代码
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 结合 CreateFormEditForm,实现表单的创建与编辑功能。

示例:SchemaForm 组件实现
xml 复制代码
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. 编辑表单与详情面板实现

EditFormDetailPanel 组件是基于 SchemaForm 的两个衍生组件,分别用于表单的编辑和详情数据的展示。

示例:EditForm 组件实现
ini 复制代码
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 组件实现
ini 复制代码
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 配置动态生成,能够根据不同的业务需求生成对应的表单和面板。此外,组件的组合(如 createFormeditForm)可以通过配置轻松地在多个地方复用。

通过这种方式,我们能够大幅度提高开发效率,避免重复造轮子,同时保证项目的可维护性和扩展性。


总结

在本部分中,我们介绍了如何基于 Vue3 构建一个动态组件库,包括 动态表单编辑表单详情面板 的实现。这些组件基于 schema 配置动态生成,提供了强大的灵活性和扩展性,适用于各种不同的业务需求。通过这种组件化和配置化的方式,我们可以大大提高开发效率,并保持代码的高复用性。

五、完成框架 NPM 包抽离封装并发布

在本文的第五部分,我们将详细介绍如何将 Elpis 框架的核心逻辑进行抽离并封装为一个 NPM 包 ,然后将其发布到 NPM 上。这样,其他项目可以直接通过 npm install @aodi/elpis 来使用这个框架,并能在自己的项目中进行集成与扩展。


1. 抽离核心代码并封装为 NPM 包

首先,我们需要将 Elpis 框架的核心代码抽离出来,使其能够作为一个独立的模块进行使用。为了实现这一点,我们将 Elpis 中的业务逻辑、服务端的框架部分以及相关的配置文件抽离到一个单独的包中。

步骤:
  1. elpis 项目中,将相关的文件移动到独立的目录下,确保核心代码与业务逻辑分离。例如,将 elpis-core 放入一个单独的文件夹。
  2. 修改 package.json 文件,为包提供名称、版本号和依赖信息。需要将框架的相关逻辑暴露出来,使其他项目能够通过 NPM 安装并使用。
perl 复制代码
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"
  }
}
  1. 通过运行 npm login 登录到 NPM,准备发布包。

    bash
    复制编辑
    npm login

  2. 然后,执行 npm publish 命令将包发布到 NPM 仓库中:

    bash
    复制编辑
    npm publish


2. 在 elpis-demo 项目中集成并使用 NPM 包

一旦 Elpis 被成功发布为 NPM 包,其他项目就可以通过 npm install @aodi/elpis 命令安装并使用它。

步骤:
  1. elpis-demo 项目中,首先通过 NPM 安装 Elpis
bash 复制代码
bash
复制编辑
npm install @aodi/elpis
  1. elpis-demo 的代码中,直接引入并使用 Elpis 提供的功能模块:
php 复制代码
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 配置:
  1. elpis-demo 中,确保 webpack.config.js 中设置了正确的别名,以便能够正确加载 elpis-demo 自定义的别名:
css 复制代码
javascript
复制编辑
module.exports = {
  resolve: {
    alias: {
       '$demo': 'demo'
    }
  }
};

4. 在 elpis-demo 中配置并发布

为了使 elpis-demo 项目更容易使用 Elpis ,你可以通过在 elpis-demopackage.json 中添加一些启动脚本,并配置不同的环境(如开发、生产等)来使得包管理更加灵活。

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 框架中,人员管理模块是最常见的模块之一。我们通过 schemaAPI 配置,结合 Elpis 提供的自动化组件,快速实现人员管理系统的增删改查(CRUD)功能。

人员管理模块的领域模型

首先,定义了一个简单的 人员管理模块 。在 elpis-demo/model/people/model.js 文件中,我们为人员管理系统配置了模型。

css 复制代码
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 接口,规范了用户的增删改查操作。

css 复制代码
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 向后端发送请求。

ini 复制代码
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 令牌并重定向到登录页面。

xml 复制代码
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 等工具,我们可以设置自动化流水线来构建和测试代码。

arduino 复制代码
shell
复制编辑
npm install --production
npm run build:prod
持续部署(CD)

CD 流程中,我们将代码部署到生产环境。通过 DockerKubernetes,我们能够实现自动化的部署和扩展,确保服务的高可用性和自动化管理。

yaml 复制代码
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) ,我们能够高效地将代码推送到生产环境,确保项目的稳定性和可扩展性。

相关推荐
小小小小宇8 分钟前
前端监测用户卡顿之INP
前端
小小小小宇13 分钟前
监测用户在浏览界面过程中的卡顿
前端
糖墨夕15 分钟前
Nest 是隐藏的“设计模式大佬”
前端
逾明1 小时前
Electron自定义菜单栏及Mac最大化无效的问题解决
前端·electron
辰九九1 小时前
Uncaught URIError: URI malformed 报错如何解决?
前端·javascript·浏览器
月亮慢慢圆1 小时前
Echarts的基本使用(待更新)
前端
芜青2 小时前
实现文字在块元素中水平/垂直居中详解
前端·css·css3
小高0072 小时前
React useMemo 深度指南:原理、误区、实战与 2025 最佳实践
前端·javascript·react.js
LuckySusu2 小时前
【js篇】深入理解类数组对象及其转换为数组的多种方法
前端·javascript