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

相关推荐
牧羊狼的狼2 小时前
React 中的 HOC 和 Hooks
前端·javascript·react.js·hooks·高阶组件·hoc
知识分享小能手4 小时前
React学习教程,从入门到精通, React 属性(Props)语法知识点与案例详解(14)
前端·javascript·vue.js·学习·react.js·vue·react
魔云连洲4 小时前
深入解析:Vue与React的异步批处理更新机制
前端·vue.js·react.js
mCell4 小时前
JavaScript 的多线程能力:Worker
前端·javascript·浏览器
超级无敌攻城狮6 小时前
3 分钟学会!波浪文字动画超详细教程,从 0 到 1 实现「思考中 / 加载中」高级效果
前端
excel7 小时前
用 TensorFlow.js Node 实现猫图像识别(教学版逐步分解)
前端
gnip7 小时前
JavaScript事件流
前端·javascript
赵得C7 小时前
【前端技巧】Element Table 列标题如何优雅添加 Tooltip 提示?
前端·elementui·vue·table组件
wow_DG7 小时前
【Vue2 ✨】Vue2 入门之旅 · 进阶篇(一):响应式原理
前端·javascript·vue.js
weixin_456904278 小时前
UserManagement.vue和Profile.vue详细解释
前端·javascript·vue.js