Vue+ElementUI+C#前后端分离:监控长耗时任务的实践

想象一下,我们正在构建一个Web应用,需要实现一个数据报告的导出功能。这听起来很简单,不是吗?但是,随着深入开发,我们意识到导出过程比预期的要复杂和耗时得多。由于报告的数据量巨大,后端需要花费相当长的时间来生成文件。最初的设计是前端发送一个请求到后端,然后等待直到文件生成完成后再返回给用户。然而,这种方法很快显示出了它的局限性:随着文件生成时间的增长,用户的等待时间也不断增加,最终导致请求超时,下载任务失败。

这个问题不仅仅是一个技术挑战,也直接影响到用户体验。用户可能因为长时间等待而感到沮丧,甚至怀疑系统的可靠性。作为一名热衷于提供优质用户体验的开发者,我们必须找到一个更好的解决方案来处理这个问题。

文章目录

  • 前言
  • 一、问题背景
    • [1.1 传统同步处理的问题](#1.1 传统同步处理的问题)
    • [1.2 具体案例:文件导出功能](#1.2 具体案例:文件导出功能)
    • [1.3 现代Web应用的需求](#1.3 现代Web应用的需求)
  • 二、技术选型与框架介绍
    • [2.1 前端:Vue和ElementUI的选择](#2.1 前端:Vue和ElementUI的选择)
      • [2.1.1 为什么选择Vue](#2.1.1 为什么选择Vue)
      • [2.1.2 为什么选择ElementUI](#2.1.2 为什么选择ElementUI)
    • [2.2 后端:为何选择C#.NET Core](#.NET Core)
    • [2.3 数据交互和API设计原则](#2.3 数据交互和API设计原则)
      • [2.3.1 RESTful API设计](#2.3.1 RESTful API设计)
      • [2.3.2 数据格式和错误处理](#2.3.2 数据格式和错误处理)
      • [2.3.3 安全性和性能](#2.3.3 安全性和性能)
  • 三、前端实现
    • [3.1 Vue组件的设计](#3.1 Vue组件的设计)
      • [3.1.1 ProgressDialog组件](#3.1.1 ProgressDialog组件)
      • [3.1.2 ElementUI组件的使用](#3.1.2 ElementUI组件的使用)
    • [3.2 发起异步任务和处理响应](#3.2 发起异步任务和处理响应)
      • [3.2.1 发起任务](#3.2.1 发起任务)
      • [3.2.2 处理响应](#3.2.2 处理响应)
    • [3.3 代码实现](#3.3 代码实现)
      • [3.3.1 progressDialog.vue](#3.3.1 progressDialog.vue)
      • [3.3.2 ProgressDialog组件调用](#3.3.2 ProgressDialog组件调用)
  • 四、后端实现
  • 总结
    • [1. 项目挑战和解决方案](#1. 项目挑战和解决方案)
    • [2. 前端和后端的协同](#2. 前端和后端的协同)
    • [3. 关键技术点](#3. 关键技术点)
    • [4. 成果与反思](#4. 成果与反思)

前言

在现代的Web应用开发中,前后端的协同工作已经成为提升用户体验和应用性能的关键。特别是在处理一些耗时的后端任务时,如何优雅地管理这些任务,不仅影响着用户的等待时间,也直接关联到系统的稳定性和可靠性。本文将通过一个实际案例,介绍如何在Vue+ElementUI的前端环境和C#.NET Core的后端环境中实现一个高效的解决方案。我们将探讨如何设计一个异步的任务处理机制,允许前端发起下载任务,然后不必等待后端任务完成就能立即得到响应,同时能够实时监控任务的进度,并在任务完成后通知用户进行下载。这种方式不仅优化了用户的等待体验,也提高了应用的性能和稳定性。


一、问题背景

在当今快速发展的Web应用领域,用户期待的不仅仅是功能完善的应用,还有流畅、响应迅速的用户体验。特别是在数据密集型的应用中,后端处理的时间成为了影响用户体验的关键因素之一。长耗时任务,如大规模数据的处理和报告的生成,常常成为前后端协作中的一个挑战点。

1.1 传统同步处理的问题

在传统的Web应用模型中,前端发出请求后,通常会同步等待后端处理完成并返回结果。这种模式在处理简单、响应快速的请求时表现良好。然而,当涉及到耗时较长的操作时,比如需要进行复杂计算或处理大量数据的报告生成,同步处理就显得力不从心。在这种情况下,用户发起的请求可能因为后端处理时间过长而导致超时,从而影响了整体的用户体验和应用的性能。

1.2 具体案例:文件导出功能

让我们以一个具体的例子来说明这个问题。在一个企业级应用中,经常需要提供数据报告的导出功能。假设这些报告包含大量的数据,并需要进行复杂的处理才能生成最终的文件。如果采用传统的同步处理方式,即用户点击"导出"按钮后,前端需要等待后端完成整个文件生成过程,这不仅会导致长时间的等待,还可能因为超时而导致失败。这不仅对用户来说是一个糟糕的体验,多次点击"导出"按钮也对后端服务器造成了不必要的压力。

1.3 现代Web应用的需求

现代Web应用的开发越来越倾向于前后端分离的架构。在这种架构下,前端负责展示和用户交互,而后端则负责数据处理和业务逻辑。这种分离带来了更好的维护性和可扩展性,但同时也要求我们在处理长耗时任务时采取更加高效和用户友好的方法。因此,寻找一种方式,让前端可以发起任务请求,而后端则异步处理这些请求,并实时反馈进度给前端,成为了一项重要的需求。

在接下来的内容中,我们将详细探讨如何在使用Vue和ElementUI的前端与C#.NET Core后端协同工作的环境中,设计和实现一个高效的长耗时任务处理机制,不仅优化用户的等待体验,同时提升系统的整体性能和稳定性。

二、技术选型与框架介绍

在构建现代Web应用时,选择合适的技术栈是关键的一步。它不仅影响着应用的开发效率和未来的可维护性,还直接关联到最终用户的体验。在我们的项目中,我们选择了Vue和ElementUI作为前端框架,而后端则采用了C#.NET Core。下面,我们将详细探讨这些技术的选择理由以及我们的数据交互和API设计原则。

2.1 前端:Vue和ElementUI的选择

2.1.1 为什么选择Vue

Vue.js是一个流行的前端JavaScript框架,它以其简洁、灵活和高效著称。Vue的主要优势包括:

  • 响应式和组件化:Vue的响应式系统和组件化模型为开发复杂的单页应用(SPA)提供了强大的支持。
  • 易于学习:相对于其他前端框架,Vue更易于上手,有助于加快开发进程。
  • 灵活性:Vue不仅适用于构建大型应用,也可以作为项目的一部分引入。

2.1.2 为什么选择ElementUI

ElementUI是一个基于Vue的组件库,专为Web应用开发而设计。其主要特点包括:

  • 丰富的组件:提供了广泛的可重用组件,如表格、对话框、菜单等,极大地提高了开发效率。
  • 易于定制:支持灵活的主题定制,可以轻松地调整组件的外观以符合品牌风格。
  • 良好的文档和社区支持:ElementUI拥有详细的文档和活跃的社区,方便开发者学习和解决问题。

2.2 后端:为何选择C#.NET Core

C#.NET Core是一个跨平台的、开源的框架,由Microsoft开发。它适用于构建各种类型的应用,从云服务到Web应用再到物联网应用。其主要优点包括:

  • 性能:.NET Core是性能优越的框架之一,尤其适合处理复杂和资源密集型的后端操作。
  • 跨平台:可在多种操作系统上运行,包括Windows、Linux和macOS。
  • 强大的生态系统:拥有广泛的库和工具支持,以及一个庞大的开发者社区。
  • 安全和成熟:作为Microsoft的核心产品之一,.NET Core在安全性和稳定性方面经过了严格的测试。

2.3 数据交互和API设计原则

在前后端分离的架构中,数据交互和API的设计至关重要。以下是我们遵循的一些设计原则:

2.3.1 RESTful API设计

  • 简洁明了的URL:使用易于理解的URL路径,反映资源的层次结构。
  • 标准化的HTTP方法:使用标准的HTTP方法(如GET、POST、PUT、DELETE)来处理不同的操作。
  • 无状态:每个请求应自包含,不依赖于服务器的上下文或状态。

2.3.2 数据格式和错误处理

  • JSON数据格式:采用JSON作为前后端交互的主要数据格式,因其轻量和易于解析。
  • 详细的错误响应:在API出现错误时,提供清晰、一致的错误响应,包括错误码和错误信息。

2.3.3 安全性和性能

  • 安全措施:实施适当的安全措施,如HTTPS、身份验证和授权。
  • 性能优化:为提高响应速度和减轻服务器负担,采取必要的性能优化措施,如分页和缓存。

通过精心选择的技术栈和遵循这些设计原则,我们的应用不仅能提供强大的功能,还能确保良好的用户体验和高效的性能。接下来,我们将深入探讨前端和后端的具体实现细节。

三、前端实现

在构建现代Web应用时,前端的作用不仅限于展示数据和界面,还涉及到与后端服务的动态交互,特别是在处理长耗时任务时。前端界面能够发起异步任务、监控其进度,并在任务完成时提供反馈。我们将通过一个具体的案例 ------ 文件导出功能的实现来演示这一过程。

3.1 Vue组件的设计

我们的目标是创建一个用户友好的界面,允许用户发起导出任务,并实时监控任务的进度。为此,我们设计了一个名为ProgressDialog的Vue组件。该组件负责显示每个导出任务的进度和状态,并提供下载完成文件的功能。

3.1.1 ProgressDialog组件

ProgressDialog组件利用了Vue的响应式特性和组件化能力,实现了以下关键功能:

  • 任务监控:通过轮询API来监控每个任务的状态和进度。
  • 用户交互:展示每个任务的详细信息,包括任务ID、文件名、当前状态和进度条。
  • 下载功能:对于完成的任务,提供一个下载按钮,允许用户下载生成的文件。

这个组件接收taskIds(任务ID数组)和dialogVisible(控制对话框显示的布尔值)作为props,使得它可以灵活地嵌入到不同的父组件中。

3.1.2 ElementUI组件的使用

ProgressDialog组件中,我们使用了以下ElementUI组件:

  • el-dialog:用于展示进度信息的对话框。
  • el-rowel-col:用于布局任务信息和下载按钮。
  • el-progress:用于展示任务进度。
  • el-button:用于触发下载操作。

这些组件的使用大大简化了UI的实现过程,同时保证了界面的美观和用户友好性。

3.2 发起异步任务和处理响应

3.2.1 发起任务

在我们的案例中,用户可以通过两种方式发起导出任务:

  1. 选择特定条目导出:用户选择特定的数据条目,点击"导出"按钮发起任务。
  2. 按条件导出:用户根据特定条件筛选数据,然后发起导出任务。

我们在abcManage组件中实现了这两种方式。通过调用相应的API(exportAbcManageexportAbcManageByCondition),我们可以获取任务ID,并使用ProgressDialog组件来显示任务进度。

3.2.2 处理响应

一旦任务被发起,ProgressDialog组件开始轮询后端服务,查询每个任务的当前状态和进度。这是通过在组件的methods中实现一个pollTaskProgress函数来完成的。该函数定期调用后端API,更新每个任务的进度和状态。

当任务完成时,组件会显示一个下载按钮。点击此按钮将触发一个下载操作,允许用户获取生成的文件。

3.3 代码实现

3.3.1 progressDialog.vue

html 复制代码
<template>
  <el-dialog title="下载进度"
             :close-on-click-modal='false'
             :visible.sync="localDialogVisible">
    <!-- 遍历任务数组 -->
    <div v-for="(task, index) in tasks"
         :key="index">
      <el-row>
        <!-- 显示任务令牌或文件名 -->
        <el-col :span="16">
          <div v-if="task.status !== '执行完成'">{{ '令牌:' + task.taskId }}</div>
          <div v-if="task.status === '执行完成'">{{ '文件:' + task.fileName }}</div>
        </el-col>
        <!-- 下载按钮 -->
        <el-col :span="8">
          <el-button v-if="task.status === '执行完成'"
                     type="primary"
                     size="mini"
                     @click="downloadFile(task.fileName)">
            下载
          </el-button>
        </el-col>
      </el-row>
      <!-- 进度条 -->
      <el-progress :percentage="task.progress"></el-progress>
      <!-- 显示任务状态 -->
      <div v-if="task.status !== '执行完成'">{{ task.status }}</div>
      <!-- 显示错误信息 -->
      <div v-if="task.status === '执行失败'">错误信息: {{ task.errorMsg }}</div>
    </div>
    <!-- 对话框底部按钮 -->
    <div slot="footer"
         class="dialog-footer">
      <el-button @click="dialogClose">关 闭</el-button>
    </div>
  </el-dialog>
</template>

<script>
export default {
  name: 'ProgressDialog',
  // 接收父组件传递的属性
  props: {
    taskIds: {
      type: Array,
      required: true
    },
    dialogVisible: {
      type: Boolean,
      required: true
    }
  },
  data () {
    return {
      localDialogVisible: this.dialogVisible, // 本地对话框显示状态
      tasks: [] // 任务数组
    }
  },
  // 监听器
  watch: {
    // 当任务ID数组变化时初始化任务和开始轮询
    taskIds (val) {
      if (val !== null && val !== undefined && val.length > 0) {
        this.initializeTasks()
        this.startPolling()
      }
    },
    // 监听对话框可见性变化
    dialogVisible (val) {
      this.localDialogVisible = val
    }
  },
  methods: {
    // 初始化任务
    initializeTasks () {
      this.tasks = this.taskIds.map(id => ({
        taskId: id,
        progress: 0,
        status: '等待执行',
        errorMsg: '',
        fileName: ''
      }))
    },
    // 开始轮询每个任务的进度
    startPolling () {
      this.tasks.forEach(task => {
        this.pollTaskProgress(task)
      })
    },
    // 轮询特定任务的进度
    pollTaskProgress (task) {
      const polling = () => {
        this.$api.download.getByTaskId(task.taskId).then(res => {
          // 更新任务状态
          task.status = res.status
          task.errorMsg = res.errorMsg
          task.currentCount = res.currentCount
          task.totalCount = res.totalCount
          task.fileName = res.fileName
          // 计算进度
          task.progress = task.totalCount > 0 ? parseFloat(Math.min(Math.max(
            (task.currentCount / task.totalCount) * 100, 0), 100).toFixed(1)) : 0

          // 如果任务未完成或失败,继续轮询
          if (task.status !== '执行完成' && task.status !== '执行失败') {
            setTimeout(polling, 1000)
          }
        })
      }
      polling()
    },
    // 下载文件
    downloadFile (fileName) {
      // 调用下载接口
      ...
      this.$message.error("调用下载接口" + fileName)
    },
    // 关闭对话框
    dialogClose () {
      // 发出关闭对话框的事件
      this.$emit('update:dialogVisible', false)
    }
  }
}
</script>

3.3.2 ProgressDialog组件调用

这段代码展示了如何在父组件 abcManage 中使用 ProgressDialog 组件。

在 Vue 模板中,ProgressDialog 组件被嵌入到父组件的模板中。它通过特定的属性(taskIdsdialogVisible)来接收必要的数据和状态。在父组件的脚本中,定义了相关的数据属性和方法,用于控制 ProgressDialog 组件的显示和数据传递。

html 复制代码
<template>
  <div class="container">
    <el-card>
      <!-- 其他内容,例如表格组件 -->
      <my-table @exportExcel="exportExcel"
                @exportByCondition="exportByCondition">
      </my-table>
    </el-card>
    <!-- ProgressDialog组件的调用 -->
    <progress-dialog ref="progressWindow"
                     :taskIds.sync="progressTaskIds"
                     :dialogVisible.sync="progressVisible"></progress-dialog>
  </div>
</template>

<script>
import MyTable from '@/components/table/index'
import ProgressDialog from '@/components/progress/progressDialog'

export default {
  name: 'abcManage',
  data () {
    return {
      // ...其他数据属性
      progressTaskIds: [], // 用于存储任务ID的数组
      progressVisible: false // 控制ProgressDialog组件的显示
    }
  },
  methods: {
    // 显示ProgressDialog组件并传递任务ID
    showProgressWindow (taskIds) {
      this.progressVisible = true
      this.progressTaskIds = taskIds
    },
    // 导出Excel的方法
    exportExcel (items) {
      // ...导出逻辑
      // 调用showProgressWindow来显示进度对话框
      this.$api.exportExcel.exportAbcManage(ids).then(res => {
         this.showProgressWindow(res)
       }).catch(_err => {
       })
    },
    // 按条件导出的方法
    exportByCondition (advanceSearch, searchData) {
      // ...导出逻辑
      // 调用showProgressWindow来显示进度对话框
      this.$api.exportExcel.exportAbcManageByCondition(searchModel).then(res => {
        this.showProgressWindow(res)
      }).catch(_err => {
      })
    }
  },
  components: {
    MyTable, ProgressDialog  // 注册子组件
  }
}
</script>

通过Vue和ElementUI的结合,我们成功实现了一个前端界面,它不仅可以发起异步任务,还能实时监控任务的进度并进行交互。这种模式不仅提高了用户体验,也使得处理长耗时任务变得更加高效和可靠。

四、后端实现

在构建一个响应式且高效的前端界面时,强大且可靠的后端实现是不可或缺的。特别是在处理长耗时任务,如文件导出等场景中,后端需要有效地管理这些任务,同时提供实时的状态更新。在这一部分,我们将讨论如何在C#.NET Core环境中实现后端逻辑,包括异步编程、任务管理与状态更新,以及文件生成与存储策略。

4.1 后端异步实现策略

4.1.1 C#.NET Core中的DDD异步编程思路

在采用领域驱动设计(DDD)的方式实现异步任务时,我们通常会将业务逻辑和数据持久化逻辑分离,以提高代码的可维护性和可测试性。以下是如何将上述的异步文件导出逻辑应用到DDD架构中的示例:

导出请求处理

在DDD中,我们会创建一个用于处理导出请求的应用服务,并在其中实现业务逻辑。

csharp 复制代码
public class ExportService : IExportService
{
    private readonly IExportTaskRepository _exportTaskRepository;
    private readonly IExportProcessor _exportProcessor;

    public ExportService(IExportTaskRepository exportTaskRepository, IExportProcessor exportProcessor)
    {
        _exportTaskRepository = exportTaskRepository;
        _exportProcessor = exportProcessor;
    }

    public async Task<string> ScheduleExportAsync(ExportRequestModel request)
    {
        var exportTask = new ExportTask(Guid.NewGuid().ToString(), request);
        await _exportTaskRepository.AddAsync(exportTask);

        // 使用后台服务或队列来处理任务
        _exportProcessor.ProcessInBackground(exportTask);

        return exportTask.Id;
    }
}
导出任务实体

在DDD中,我们将导出任务建模为一个领域实体。

csharp 复制代码
public class ExportTask
{
    public string Id { get; private set; }
    public ExportRequestModel Request { get; private set; }
    // 其他属性如状态、进度等

    public ExportTask(string id, ExportRequestModel request)
    {
        Id = id;
        Request = request;
        // 初始化状态等
    }

    // 更新状态的方法
    public void UpdateStatus(/* ... */)
    {
        // ...
    }
}
导出任务仓储接口

在DDD中,我们定义一个仓储接口,用于封装导出任务的持久化逻辑。

csharp 复制代码
public interface IExportTaskRepository
{
    Task AddAsync(ExportTask task);
    Task<ExportTask> GetByIdAsync(string id);
    // 其他持久化操作
}
导出处理逻辑

导出处理逻辑可以被封装在一个单独的服务中,该服务可能使用后台任务或消息队列来处理实际的导出任务。

csharp 复制代码
public interface IExportProcessor
{
    void ProcessInBackground(ExportTask task);
}

public class ExportProcessor : IExportProcessor
{
    public void ProcessInBackground(ExportTask task)
    {
        Task.Run(async () =>
        {
            // 实际的导出逻辑
            // ...
            // 更新任务状态
            // ...
        });
    }
}

通过这种方式,我们将业务逻辑、数据持久化以及实际的导出处理逻辑分离开,使得每个部分都更加专注于它们的职责。这样不仅提高了代码的可维护性,也使得整个系统更容易测试和扩展。

4.1.2 任务管理与状态更新

在处理Web应用的后端逻辑,特别是涉及长耗时操作如文件导出时,任务管理与状态更新成为了异步处理流程的核心。有效地管理任务状态不仅关系到用户体验的优化,还直接影响系统的整体性能。

在我们的系统中,为了跟踪和管理长时间运行的任务,我们创建了一个名为 DownloadCenterEntity 的实体类,用于表示下载中心的相关信息。这个类在数据库中对应一张表,用于存储各种任务的状态和进度信息。

实体类设计
csharp 复制代码
namespace AbcErp.Core.Entity.Download
{
    /// <summary>
    /// 下载中心
    /// </summary>
    [Table("erp_sys_download_center")]
    [Index(nameof(TaskId), IsUnique = true)]
    public class DownloadCenterEntity : IEntity
    {
        // ...属性定义省略
    }
}

DownloadCenterEntity 类中,我们定义了几个关键属性:

  • TaskId: 任务的唯一标识符,用于追踪每个独立的任务。
  • Status: 表示任务的当前状态,如"等待执行"、"执行中"、"执行完成"或"执行失败"。
  • CurrentCountTotalCount: 分别表示任务当前完成的进度和总进度,用于计算任务的完成百分比。
  • ErrorMsg: 存储任务失败时的错误信息。
  • FileName: 生成的文件名称,用于下载操作。
任务状态管理的重要性

通过在数据库中存储任务的状态和进度信息,我们可以实现几个关键功能:

  1. 实时进度反馈:前端可以定期查询这些信息,为用户提供实时的任务进度反馈。
  2. 错误处理:如果任务失败,错误信息可以帮助开发者快速定位问题。
  3. 数据一致性:使用数据库存储状态信息,确保了在多用户访问或服务重启的情况下,任务信息的一致性和完整性。
异步任务处理流程

在实际的业务流程中,当用户请求生成报表或执行其他长耗时操作时,后端服务会创建一个新的 DownloadCenterEntity 实例,设置初始状态,并保存到数据库中。随后,后端服务启动一个异步任务来处理实际的业务逻辑,同时定期更新数据库中的任务状态和进度信息。

例如,对于一个文件导出操作,后端服务会先设置任务状态为"等待执行",然后开始生成文件。文件生成过程中,服务会更新任务的当前进度,一旦完成,则更新状态为"执行完成"并保存文件名称用于下载。

在C#.NET Core环境下,通过结合实体类设计和数据库操作,我们能够有效地管理和跟踪后端的长耗时任务。这种方式不仅提高了系统的响应性和稳定性,也优化了用户体验,使用户能够实时了解任务进度和结果。通过这种方法,即使在面临复杂和资源密集型的任务时,我们的应用仍能保持高效和稳定的运行。

4.1.3 文件生成与存储策略

在处理文件导出等耗时操作时,后端不仅要负责生成文件,还需要考虑文件的存储和传输。这里有几个关键点需要考虑:

  1. 文件生成:根据业务需求生成相应的文件,如Excel、PDF等。

  2. 文件存储:生成的文件需要被存储在服务器上或云存储服务中。

  3. 文件访问:一旦文件被生成和存储,后端需要提供一种机制来允许前端下载这些文件。这通常通过生成一个预签名的URL来实现,前端用户可以通过这个URL安全地下载文件。

结合这些策略,C#.NET Core后端不仅能高效地处理耗时任务,还能提供必要的状态信息给前端,确保整个系统的响应性和用户体验。

4.2 后端异步实践

4.2.1 Web API 接口设计

在C#.NET Core项目中设计Web API接口,以支持高效的导出任务处理,具体聚焦于 ExportExcel 类中定义的两个关键接口。

在我们的示例项目 AbcErp 中,ExportExcel 类扮演着核心角色,提供了导出数据为Excel文件的功能。该类不仅包含了必要的服务依赖注入,还定义了两个关键的Web API方法,分别用于处理基于ID的导出请求和基于搜索条件的导出请求。

服务类依赖注入

ExportExcel 类通过构造函数注入 IAbcExportService 服务,这使得它可以利用该服务来处理实际的导出逻辑。这种依赖注入方式确保了类的职责清晰分离,便于维护和测试。

csharp 复制代码
public ExportExcel(IAbcExportService abcExportExport)
{
    _abcExportExport = abcExportExport;
}
导出基于Id的数据

ExportAbcManageToExcel 方法提供了一个POST接口,允许前端传入一组ID,以选择特定的数据进行导出。

csharp 复制代码
[HttpPost("exportAbcManage")]
public async Task<List<string>> ExportAbcManageToExcel(List<int> ids)
{
    return await _abcExportExport.ExportAbcManageByIds(ids);
}

这个方法的实现是异步的,这意味着它可以处理耗时的操作而不会阻塞服务器的其他进程。方法返回一个字符串列表,通常包含任务的唯一标识符,前端可以使用这些标识符来轮询任务的状态和进度。

导出基于条件的数据

另一个接口 ExportAbcManageByConditionToExcel 类似地提供了数据导出功能,但它允许用户根据复杂的搜索条件来筛选需要导出的数据。

csharp 复制代码
[HttpPost("exportAbcManageByCondition")]
public async Task<List<string>> ExportAbcManageByConditionToExcel(AbcManageAdvSrhDto searchDto)
{
    return await _abcExportExport.ExportAbcManageByCondition(searchDto);
}

这种灵活性是现代Web应用的一个重要特点。它不仅提供了用户友好的体验,也允许后端更有效地处理大量数据。

4.2.2 服务与仓储设计

我们首先定义了IAbcExportService接口,负责声明文件导出相关的业务逻辑。然后,我们实现了该接口的具体逻辑,在AbcExportService类中处理文件导出的具体任务。

服务接口
csharp 复制代码
public interface IAbcExportService
{
    Task<List<string>> ExportAbcManageByIds(List<int> ids);
    Task<List<string>> ExportAbcManageByCondition(AbcManageAdvSrhDto searchDto);
}
服务实现

在服务实现中,我们使用了依赖注入来引入所需的仓储和其他服务,如下所示:

csharp 复制代码
public class AbcExportService : IAbcExportService, IScoped
{
    private readonly IRepository<DownloadCenterEntity> _downloadRepository;
    private readonly IHttpContextAccessor _httpContext;
    private readonly IServiceScopeFactory _scopeFactory;
    private readonly OssUtil _ossService;

    // 构造函数省略...

    // 业务逻辑方法省略...
}

4.2.3 异步任务处理

异步任务的核心在于不阻塞主线程,同时有效地管理长时间运行的操作。在我们的实现中,每个导出操作被视为一个单独的任务,由后台线程处理。

异步任务启动

我们在服务中定义了方法来启动异步任务,如下所示:

csharp 复制代码
public async Task<List<string>> ExportAbcManageByIds(List<int> ids)
{
    // 任务初始化和启动逻辑
    // ...
    await Task.Run(() => PrepareAbcManageByIds(taskId, ids));
    return new List<string> { taskId };
}
任务处理逻辑

PrepareAbcManageByIds方法中,我们处理实际的导出逻辑。这包括从数据库获取数据,生成Excel文件,上传到OSS(对象存储服务),并更新任务状态。

csharp 复制代码
private async void PrepareAbcManageByIds(string taskId, List<int> ids)
{
    using (var scope = _scopeFactory.CreateScope())
    {
        // 获取依赖服务
        var repository = scope.ServiceProvider.GetRequiredService<IRepository<AbcProductEntity>>();
        // 数据处理和文件生成逻辑
        // ...
    }
}

在.NET Core中,使用 var scope = _scopeFactory.CreateScope() 创建一个新的服务作用域 (IServiceScope) 是一种常见的做法,特别是在需要在后台任务或非HTTP请求的上下文中使用依赖注入时。这个做法的原因和优势如下:

1. 服务生命周期和作用域

.NET Core中的服务(即依赖项)可以有不同的生命周期:Singleton(单例)、Scoped(作用域)和Transient(短暂)。其中,Scoped生命周期意味着服务的实例是针对每个请求作用域创建的。在HTTP请求的上下文中,这个作用域自然对应于一个请求的生命周期。

2. 后台任务中的服务作用域问题

当在后台任务或非HTTP请求的上下文中运行代码时,如在一个新开启的线程或使用 Task.Run() 时,就处于请求作用域之外。在这种情况下,直接使用依赖注入的Scoped服务会有问题,因为这些服务是为每个HTTP请求而创建的,而在后台任务中并不存在这样的请求作用域。

3. 使用 IServiceScopeFactory创建新的作用域

为了解决这个问题,IServiceScopeFactory 被用来创建一个新的作用域。在这个新的作用域中,可以获取到Scoped服务的新实例。这意味着我们可以在后台任务中安全地使用这些服务,而不用担心它们是为特定HTTP请求创建的。

在示例代码 PrepareAbcManageByIds 方法中,使用 IServiceScopeFactory 创建新作用域的原因是:

  • 方法可在后台线程中运行,与原始的HTTP请求无关。
  • 通过创建新的作用域,能够安全地解析和使用Scoped服务,如数据库仓储(IRepository<AbcProductEntity>)。
  • 这样确保了每个后台任务都有自己独立的服务实例,避免了潜在的并发问题,同时也遵循了依赖注入和服务生命周期的最佳实践。

使用 IServiceScopeFactory 创建新的服务作用域是处理后台任务中的Scoped服务的标准做法,它既保持了服务生命周期的一致性,又提供了代码运行时所需的灵活性和安全性。

4.2.4 文件生成与上传

文件生成和上传是导出任务中最关键的部分。我们使用了NPOI库来生成Excel文件,并利用OSS服务来存储和分享生成的文件。

Excel文件生成

GenerateAbcManage方法中,我们将查询到的数据转换为Excel文件:

csharp 复制代码
private async Task GenerateAbcManage(string taskId, List<AbcProductEntity> entities, ...)
{
    // 使用NPOI生成Excel文件
    // ...
}
文件上传至OSS

生成的Excel文件随后被上传至OSS,以便用户可以下载:

csharp 复制代码
var uploadres = _ossService.UploadStreamToOss(ms, key);
// 错误处理和状态更新逻辑
// ...

通过这种方式,我们能够有效地处理大量数据的导出请求,同时保持应用的响应性。这种异步和任务分离的方法在处理资源密集型任务时尤为重要,不仅提高了系统的效率,还增强了用户体验。在C#.NET Core的环境下,借助DDD原则和异步编程,我们可以构建出既强大又灵活的后端服务。

4.2.5 部分代码片段演示

API 接口
csharp 复制代码
[Route("/abc/erp/ExportExcel")]
[ApiDescriptionSettings("ExportExcel", Tag = "ExportExcel", Version = "v0.0.1")]
[DynamicApiController]
public class ExportExcel: IExportExcel
{
    // 其他代码 ...
    private readonly IAbcExportService _abcExportExport;

    public ExportExcel(
        // 其他代码 ...
        IAbcExportService abcExportExport
        
    {
       // 其他代码 ...
        _abcExportExport = abcExportExport;
    }

    /// <summary>
    /// 对账单(根据选中的id导出)
    /// </summary>
    [HttpPost("exportAbcManage")]
    public async Task<List<string>> ExportAbcManageToExcel(List<int> ids)
    {
        List<string> str = await _abcExportExport.ExportAbcManageByIds(ids);
        return str;

    }

    /// <summary>
    /// 对账单(根据查询的结果导出)
    /// </summary>
    [HttpPost("exportAbcManageByCondition")]
    public async Task<List<string>> ExportAbcManageByConditionToExcel(AbcManageAdvSrhDto searchDto)
    {
        List<string> str = await _abcExportExport.ExportAbcManageByCondition(searchDto);
        return str;

    }
    // 其他代码 ...

AbcErp 应用中,ExportExcel 类的设计体现了现代Web API的几个关键原则:清晰的职责分离、异步处理以及灵活的用户输入处理。通过这样的设计,我们能够提供一个高效且用户友好的导出功能,无论是处理基于ID的简单导出请求,还是满足复杂的条件筛选导出需求。这不仅提升了应用的性能,也优化了用户体验,使得数据管理和报告生成变得更加高效和便捷。

异步服务
csharp 复制代码
public class AbcExportService : IAbcExportService, IScoped
{
    private readonly IRepository<DownloadCenterEntity> _downloadRepository;
    private readonly IHttpContextAccessor _httpContext;
    private readonly IServiceScopeFactory _scopeFactory;

    private readonly OssUtil _ossService;
    private ExcelStyle NopiCommon = new ExcelStyle();

    /// <summary>
    /// 构造函数
    /// </summary>
	public AbcExportService(
	    IRepository<DownloadCenterEntity> downloadRepository,
	    IHttpContextAccessor httpContext,
	    IServiceScopeFactory scopeFactory,
	    OssUtil ossService
		)
	{
	    _downloadRepository = downloadRepository; // 用于数据存储和检索的仓储
	    _httpContext = httpContext; // 用于获取当前HTTP上下文信息
	    _scopeFactory = scopeFactory; // 用于创建服务作用域
	    _ossService = ossService; // OSS服务工具类
	}

    public async Task<List<string>> ExportAbcManageByIds(List<int> ids)
    {
	    List<string> str = new List<string>(); // 存储任务ID的列表
	    var userName = _httpContext.HttpContext.User.GetUserName(); // 获取当前用户名称
	    var taskId = Guid.NewGuid().ToString(); // 生成唯一任务标识
	
	    // 确保生成的任务ID是唯一的
	    while (true)
	    {
	        int iCount = await _downloadRepository.Where(x => x.TaskId.Equals(taskId)).CountAsync();
	        if (iCount == 0)
	        {
	            break;
	        }
	        else
	        {
	            taskId = Guid.NewGuid().ToString(); // 重新生成任务标识
	        }
	    }
	
	    // 创建并保存下载任务信息
        DownloadCenterEntity downloadCenter = new DownloadCenterEntity();
	    // ... 设置任务的各种属性
        downloadCenter.UserName = userName;
        downloadCenter.TaskId = taskId;
        downloadCenter.TaskName = "对账报表-导出勾选结果";
        downloadCenter.SearchCondition = JsonConvert.SerializeObject(ids);
        downloadCenter.InterfaceName = "ExportAbcManageByIds";
        downloadCenter.Status = "等待执行";
        downloadCenter.CurrentCount = 0;
        downloadCenter.TotalCount = 100; // 防止除0
        downloadCenter.CreateTime = DateTime.Now;
        downloadCenter.CompleteTime = DateTime.Now;
        await _downloadRepository.InsertNowAsync(downloadCenter);

        await Task.Run(() => PrepareAbcManageByIds(taskId, ids)); // 在后台异步启动导出任务

        str.Add(taskId);
        return str;
    }

    public async Task<List<string>> ExportAbcManageByCondition(AbcManageAdvSrhDto searchDto)
    {
        // 方法类似
    }


    private async void PrepareAbcManageByIds(string taskId, List<int> ids)
    {
	    using (var scope = _scopeFactory.CreateScope())
	    {
	        // 使用服务作用域获取必要的依赖服务
	        var repository = scope.ServiceProvider.GetRequiredService<IRepository<AbcProductEntity>>();
	        var downloadRepository = scope.ServiceProvider.GetRequiredService<IRepository<DownloadCenterEntity>>();
	        var mapper = scope.ServiceProvider.GetRequiredService<IMapper>();
	
	        DownloadCenterEntity downloadCenter = await downloadRepository.Where(x => x.TaskId.Equals(taskId)).FirstOrDefaultAsync();
	        if (downloadCenter == null)
	        {
	        	// 如果找不到任务实体,直接返回
	        	// 更新任务状态
	            return; 
	        }
	        downloadCenter.Status = "准备数据"; // 更新任务状态
	        await downloadRepository.UpdateNowAsync(downloadCenter);
	
	        // 执行耗时操作:查询所有实体集合
	        var abcPrds = await repository.Where(x => ids.Contains(x.Id)).AsNoTracking().ToListAsync();
	
	        // ... 处理查询结果和错误情况
            await downloadRepository.UpdateNowAsync(downloadCenter);
            await GenerateAbcManage(taskId, abcPrds, downloadRepository, mapper);
	    }
    }

    private async void PrepareAbcManageByCondition(string taskId, AbcManageAdvSrhDto searchDto)
    {
        // 方法类似
    }

    private async Task GenerateAbcManage(string taskId, List<AbcProductEntity> abcPrds, IRepository<DownloadCenterEntity> downloadRepository, IMapper mapper)
    {
    	// ... 获取任务信息和准备数据
        try
        {
	        // 渲染任务并生成Excel文件
	        // ... Excel文件生成逻辑
			// 渲染任务  长耗时任务
			// 10行更新一次记录
			if (z % 10 == 0)
			{
			    downloadCenter.CurrentCount = z;
			    await downloadRepository.UpdateNowAsync(downloadCenter);
			}
	        // 上传文件到OSS
            MemoryStream ms = new MemoryStream();
            workbook.Write(ms);
            ms.Flush();
	        var uploadres = _ossService.UploadStreamToOss(ms, key);
            if (!uploadres.Res)
            {
            	// 如果上传失败,更新任务状态为执行失败
                downloadCenter.Status = "执行失败";
                downloadCenter.ErrorMsg = "导出文件失败!";
                downloadCenter.CompleteTime = DateTime.Now;
                await downloadRepository.UpdateNowAsync(downloadCenter);
				return;
            }
        	// 更新任务完成状态
            downloadCenter.CurrentCount = abcMans.Count();
            downloadCenter.FileName = fileName;
            downloadCenter.Status = "执行完成";
            downloadCenter.CompleteTime = DateTime.Now;
            await downloadRepository.UpdateNowAsync(downloadCenter);
        }
        catch (Exception ex)
        {
	        // 出现异常时,更新任务状态为执行失败
	        // ...
            downloadCenter.Status = "执行失败";
            downloadCenter.ErrorMsg = ex.Message;
            downloadCenter.CompleteTime = DateTime.Now;
            await downloadRepository.UpdateNowAsync(downloadCenter);
        }
        finally
        {
        	// 最终处理
        }
    }
}

总结

1. 项目挑战和解决方案

在开发一个Web应用的数据报告导出功能时,我们面临了一个显著的技术挑战:如何高效地处理大量数据并生成文件,同时提供良好的用户体验。最初的同步处理方式导致了长时间的等待和请求超时,这对用户体验产生了负面影响。为了解决这一问题,我们采用了一系列技术策略和最佳实践,包括前后端分离、异步处理、服务优化,以及API的设计。

2. 前端和后端的协同

前端实现

我们选择了Vue和ElementUI作为前端框架和UI组件库,以其简洁性和灵活性来提高开发效率。特别是ProgressDialog组件的设计,使得前端能够以用户友好的方式展示异步任务的进度和状态。

后端架构

后端选用C#.NET Core,不仅因为其跨平台和高性能特性,而且因为它在处理异步操作和大量数据方面的强大能力。我们采用了领域驱动设计(DDD)来组织代码结构,提高了代码的可维护性和可测试性。

3. 关键技术点

  1. 异步编程: 在后端,我们利用了.NET Core的异步编程特性,确保了长时间运行的任务不会阻塞主线程,从而提高了应用的响应性和吞吐量。

  2. 任务管理: 通过在数据库中跟踪每个任务的状态和进度,我们能够为用户提供实时的反馈,同时保持数据的一致性和完整性。

  3. RESTful API设计: 我们精心设计了API,以支持灵活的数据导出需求,同时确保了接口的易用性和安全性。

  4. 文件处理策略: 在生成和存储文件方面,我们采用了高效的方法,包括使用NPOI库生成Excel文件,以及将文件上传到阿里云OSS进行存储和分发。

4. 成果与反思

通过这些技术实践,我们不仅优化了数据导出功能的性能,还提升了用户体验。这个项目展示了现代Web应用开发中前后端协同工作的重要性,以及在面对复杂业务需求时灵活应用技术解决方案的必要性。未来,我们还可以探索更多的优化方法,如进一步的性能调优、代码重构,以及引入新的技术栈以应对日益增长的数据处理需求。

相关推荐
cs_dn_Jie23 分钟前
钉钉 H5 微应用 手机端调试
前端·javascript·vue.js·vue·钉钉
开心工作室_kaic1 小时前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js
有梦想的刺儿1 小时前
webWorker基本用法
前端·javascript·vue.js
customer082 小时前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
getaxiosluo3 小时前
react jsx基本语法,脚手架,父子传参,refs等详解
前端·vue.js·react.js·前端框架·hook·jsx
理想不理想v3 小时前
vue种ref跟reactive的区别?
前端·javascript·vue.js·webpack·前端框架·node.js·ecmascript
小码编匠4 小时前
一款 C# 编写的神经网络计算图框架
后端·神经网络·c#
栈老师不回家4 小时前
Vue 计算属性和监听器
前端·javascript·vue.js
前端啊龙4 小时前
用vue3封装丶高仿element-plus里面的日期联级选择器,日期选择器
前端·javascript·vue.js