news 2026/8/19 22:29:46

基于EasyUI与KnockoutJS的通用分页查询与数据导出ViewModel设计

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于EasyUI与KnockoutJS的通用分页查询与数据导出ViewModel设计

1. 项目缘起:从重复劳动到统一抽象

在基于EasyUI、KnockoutJS和MVC 4.0技术栈的中后台管理系统中,分页查询和数据导出几乎是每个列表页面的标配功能。回想几年前,我接手一个项目,光是用户管理、订单管理、日志查询等模块,就写了不下十个大同小异的viewModel。每个viewModel里都充斥着几乎一模一样的currentPagepageSizetotalsearchKeyword这些属性,以及loadDatasearchexportData这些方法。更头疼的是,每个页面的导出逻辑还要单独处理列头映射、数据格式转换和文件流输出,代码重复率极高,维护起来苦不堪言。

当时我就想,能不能设计一个共通的、足够灵活的viewModel基类,把分页查询、条件筛选、数据绑定、导出Excel这些脏活累活都封装起来?让开发新页面时,只需要关心业务字段和查询接口,其他通用逻辑全部复用。这个想法经过几个项目的迭代打磨,最终形成了一个稳定、高效的解决方案。它不仅仅是一个代码片段,更是一种针对特定技术栈(EasyUI + KnockoutJS + MVC)的前后端协作模式的设计。今天,我就把这个“偷懒”的智慧分享出来,你会发现,搞定所有的分页列表和导出,真的只需要一个viewModel

2. 技术栈选型与协作模式解析

为什么是EasyUI、KnockoutJS和MVC 4.0这个“经典组合”?这背后是特定历史时期和技术背景下的最优解。EasyUI提供了丰富的、开箱即用的UI组件,尤其是它的datagrid控件,对分页、排序、筛选有着原生且强大的支持,能极大减少前端UI开发量。KnockoutJS的MVVM模式,则是连接EasyUI数据网格与后端数据的完美桥梁,它的双向数据绑定和计算属性,让页面状态(如查询条件、分页参数)的管理变得异常清晰和响应式。而MVC 4.0作为后端框架,其清晰的模型、视图、控制器分离,以及强大的模型绑定和ActionResult机制,使得构建RESTful风格的API来服务前端数据请求变得非常顺手。

这个组合的核心协作流程是这样的:

  1. 前端(View):使用EasyUI的datagrid定义表格列、分页工具栏。
  2. 前端(ViewModel):使用KnockoutJS创建一个viewModel,它包含:
    • 数据属性ko.observableArray()绑定表格数据,ko.observable()绑定当前页、页大小、总记录数、查询关键词等。
    • 行为方法loadPageData(加载数据)、search(搜索)、export(导出)等。
  3. 通信viewModel的方法通过jQuery Ajax调用后端MVC Controller的Action。
  4. 后端(Controller):接收前端传入的分页和查询参数,调用Service层获取分页数据(通常借助类似MyBatis-Plus的分页插件),封装成一个包含List<T>数据列表和total总数的统一响应模型(如PageResult<T>),以Json形式返回。
  5. 数据绑定:Ajax成功回调后,用返回的数据更新viewModelobservableArrayobservable属性,KnockoutJS自动触发UI更新,EasyUIdatagrid自动重新渲染。

理解了这套流程,我们就能找到抽象的共性:无论什么业务实体,分页查询都需要页码、页大小、总数、数据列表这几个核心属性;都需要加载、搜索、重置、导出这几个核心行为。我们的目标就是把它们抽出来。

3. 构建共通ViewModel基类的核心设计

设计一个优秀的基类,关键在于平衡通用性与灵活性。它要封装所有通用逻辑,又要允许子类轻松定制业务特有的部分。下面是我们设计的BasePagingViewModel的核心结构。

3.1 定义核心的响应式数据属性

这些属性是分页组件的状态核心,全部使用KnockoutJS的observableobservableArray进行包装,以确保UI的自动响应。

// BasePagingViewModel.js (抽象基类) function BasePagingViewModel(options) { var self = this; // 核心分页数据 self.dataList = ko.observableArray([]); // 当前页数据列表 self.currentPage = ko.observable(1); // 当前页码 self.pageSize = ko.observable(20); // 每页条数 self.totalCount = ko.observable(0); // 总记录数 self.totalPages = ko.pureComputed(function() { // 计算总页数 var size = self.pageSize(); return size === 0 ? 0 : Math.ceil(self.totalCount() / size); }); // 查询条件(这是一个空对象,被子类扩展) self.searchParams = ko.observable({}); // 加载状态,用于控制加载动画 self.isLoading = ko.observable(false); // 选中的行(用于批量操作) self.selectedItems = ko.observableArray([]); // 从options合并配置,如默认的pageSize或查询URL self.defaultPageSize = (options && options.defaultPageSize) || 20; self.dataLoadUrl = options.dataLoadUrl; // 加载数据的API地址 self.exportUrl = options.exportUrl; // 导出数据的API地址 // 初始化 self.pageSize(self.defaultPageSize); }

设计要点

  • totalPages使用ko.pureComputed自动根据totalCountpageSize计算,这是一个典型的KnockoutJS优势,无需手动维护。
  • searchParams设计为一个可观察对象,子类可以通过扩展它来添加具体的查询字段,如userName: ko.observable('')
  • dataLoadUrlexportUrl通过构造函数参数传入,保证了基类不关心具体的后端接口地址,职责分离。

3.2 实现通用的数据加载与搜索方法

这是viewModel的“发动机”,负责与后端交互,获取并绑定数据。

BasePagingViewModel.prototype.loadPageData = function (page) { var self = this; var targetPage = page || self.currentPage(); // 构建请求参数 var params = { pageIndex: targetPage, pageSize: self.pageSize() }; // 合并具体的查询条件 ko.utils.extend(params, ko.toJS(self.searchParams)); // ko.toJS将observable转换为普通JS对象 // 显示加载状态 self.isLoading(true); $.ajax({ url: self.dataLoadUrl, type: 'GET', // 分页查询通常用GET,符合RESTful风格 data: params, dataType: 'json', success: function (result) { if (result && result.success) { // 假设后端返回格式为 { success: true, data: { list: [...], total: 100 } } self.dataList(result.data.list); self.totalCount(result.data.total); self.currentPage(targetPage); // 更新当前页码 } else { $.messager.alert('错误', result.message || '数据加载失败'); } }, error: function (xhr, status, error) { $.messager.alert('错误', '网络请求异常: ' + error); }, complete: function () { self.isLoading(false); // 无论成功失败,都结束加载状态 } }); }; // 搜索:重置到第一页并加载 BasePagingViewModel.prototype.search = function () { this.currentPage(1); this.loadPageData(1); }; // 重置搜索条件并重新搜索 BasePagingViewModel.prototype.resetSearch = function () { // 清空searchParams中所有observable字段的值 var params = this.searchParams(); for (var key in params) { if (ko.isObservable(params[key]) && typeof params[key] === 'function') { params[key](''); // 重置为默认值,如空字符串 } } this.searchParams.valueHasMutated(); // 通知KnockoutJS该observable对象已变更 this.search(); };

关键经验

  1. 参数合并:使用ko.toJS(self.searchParams)是关键一步。它将一个可能包含多个observable属性的对象,转换成一个普通的键值对对象,方便通过Ajax发送。
  2. 加载状态管理isLoading属性非常有用,可以在界面上绑定一个加载动画(比如覆盖在datagrid上的半透明层),提升用户体验。
  3. 错误处理统一化:使用EasyUI的$.messager.alert进行统一提示,避免在每个Ajax回调里写重复的alert

3.3 集成EasyUI Datagrid与KnockoutJS的绑定技巧

如何让这个通用的viewModel与EasyUI的datagrid控件协同工作?这里有两种主流且优雅的方式。

方式一:通过KO Binding Handler自定义绑定(推荐)这种方式最符合KnockoutJS的哲学,声明式地将viewModel属性绑定到datagrid的配置上。

// 注册一个自定义绑定处理器 ko.bindingHandlers.easyuiDatagrid = { init: function(element, valueAccessor, allBindings, viewModel, bindingContext) { // 初始化datagrid var options = ko.utils.unwrapObservable(valueAccessor()) || {}; $(element).datagrid(options); // 将datagrid实例挂载到元素上,方便后续访问 var grid = $(element).datagrid('getPanel'); $(element).data('datagrid-instance', $(element)); // 处理分页栏的翻页事件,将其关联到viewModel var pager = $(element).datagrid('getPager'); $(pager).pagination({ onSelectPage: function(pageNum, pageSize) { viewModel.currentPage(pageNum); viewModel.pageSize(pageSize); viewModel.loadPageData(pageNum); } }); }, update: function(element, valueAccessor, allBindings, viewModel, bindingContext) { // 当viewModel.dataList变化时,刷新datagrid数据 var grid = $(element).data('datagrid-instance'); if (grid) { var options = ko.utils.unwrapObservable(valueAccessor()); // 获取最新的数据源配置 var dataOptions = $.extend(true, {}, options, { data: viewModel.dataList(), pagination: { total: viewModel.totalCount() } }); // 此处更优雅的做法是只调用loadData方法,避免重复初始化 // $(element).datagrid('loadData', viewModel.dataList()); // $(element).datagrid('getPager').pagination('refresh', {total: viewModel.totalCount()}); } } };

在HTML中,你可以这样简洁地绑定:

<table>function MyViewModel() { BasePagingViewModel.call(this, { dataLoadUrl: '/User/GetPageList' }); // ... 扩展searchParams } MyViewModel.prototype.initGrid = function (gridSelector) { var self = this; $(gridSelector).datagrid({ pagination: true, rownumbers: true, fitColumns: true, singleSelect: false, data: self.dataList(), pageNumber: self.currentPage(), pageSize: self.pageSize(), pageList: [10, 20, 50], onSelectPage: function (pageNum, pageSize) { self.currentPage(pageNum); self.pageSize(pageSize); self.loadPageData(pageNum); }, onCheckAll: function (rows) { self.selectedItems(rows); }, onUncheckAll: function () { self.selectedItems([]); }, onCheck: function (index, row) { /* 更新selectedItems */ }, onUncheck: function (index, row) { /* 更新selectedItems */ } }); // 将datagrid实例保存,方便后续如导出时获取选中行 self.dataGrid = $(gridSelector); };

对比与选择

  • 自定义绑定更优雅、声明式,与KnockoutJS生态融合更好,适合复杂或需要复用的场景。
  • 直接初始化更简单直观,易于理解和调试,适合快速开发或对KnockoutJS高级特性不熟悉的团队。 在实际项目中,我通常推荐方式一,因为它真正实现了UI声明与逻辑的分离。但方式二在遗留项目改造或简单页面中也非常有效。

4. 一站式数据导出功能的深度封装

数据导出是另一个高频且易重复的功能。我们将导出逻辑也封装在基类中,目标是:前端一行代码触发,后端一个通用方法处理,自动匹配查询条件和列头。

4.1 前端ViewModel中的通用导出方法

我们在BasePagingViewModel中添加一个exportData方法。

BasePagingViewModel.prototype.exportData = function (exportType, fileName) { var self = this; // 组装导出参数:当前查询条件 + 分页参数(通常导出全部,所以pageSize可以很大或为null) var params = ko.toJS(self.searchParams); params.pageIndex = 1; params.pageSize = 1000000; // 或者不传pageSize,由后端决定导出全部 params.exportType = exportType || 'excel'; // 导出类型,如'excel', 'pdf' params.fileName = fileName || ('导出数据_' + new Date().getTime()); // 如果需要导出选中项,可以增加逻辑 // if (self.selectedItems() && self.selectedItems().length > 0) { // params.exportIds = ko.utils.arrayMap(self.selectedItems(), function(item) { return item.id; }); // params.pageSize = undefined; // 如果导出选中项,则忽略分页参数 // } // 使用表单提交方式,避免GET请求的URL长度限制,也方便传递复杂参数 var form = $('<form>', { action: self.exportUrl, method: 'POST', style: 'display: none;' }); for (var key in params) { if (params.hasOwnProperty(key)) { $('<input>', { type: 'hidden', name: key, value: params[key] }).appendTo(form); } } $(document.body).append(form); form.submit(); setTimeout(function() { form.remove(); }, 100); };

为什么用表单提交而不是Ajax?因为文件下载需要触发浏览器的下载行为。Ajax请求的响应是JavaScript处理的,无法直接触发文件保存对话框。表单提交会引发页面导航到一个文件流,浏览器自然会处理下载。

4.2 后端MVC Controller的通用导出Action

后端的核心是动态构建Excel。我们可以使用NPOI、EPPlus或ClosedXML等库。这里以NPOI为例,展示一个高度通用的导出方法。

// BaseController.cs (或一个独立的ExportService) public abstract class BaseController : Controller { /// <summary> /// 通用数据导出方法 /// </summary> /// <typeparam name="T">导出数据的实体类型</typeparam> /// <param name="data">要导出的数据列表</param> /// <param name="columnMappings">列映射配置(列头名-属性名-格式化函数)</param> /// <param name="fileName">导出文件名</param> protected FileResult ExportToExcel<T>(List<T> data, List<ColumnMapping> columnMappings, string fileName = "导出数据") { IWorkbook workbook = new XSSFWorkbook(); // 创建Excel2007+工作簿 ISheet sheet = workbook.CreateSheet("Sheet1"); // 1. 创建标题行 IRow headerRow = sheet.CreateRow(0); for (int i = 0; i < columnMappings.Count; i++) { ICell cell = headerRow.CreateCell(i); cell.SetCellValue(columnMappings[i].HeaderText); // 可以设置标题样式... } // 2. 填充数据行 for (int rowIdx = 0; rowIdx < data.Count; rowIdx++) { IRow dataRow = sheet.CreateRow(rowIdx + 1); T item = data[rowIdx]; for (int colIdx = 0; colIdx < columnMappings.Count; colIdx++) { var mapping = columnMappings[colIdx]; ICell cell = dataRow.CreateCell(colIdx); object value = GetPropertyValue(item, mapping.PropertyName); // 应用格式化函数(如果存在) if (mapping.Formatter != null) { value = mapping.Formatter(value); } SetCellValue(cell, value); } } // 3. 自动调整列宽(按内容) for (int i = 0; i < columnMappings.Count; i++) { sheet.AutoSizeColumn(i); } // 4. 输出到HTTP响应流 MemoryStream ms = new MemoryStream(); workbook.Write(ms); ms.Seek(0, SeekOrigin.Begin); return File(ms, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", $"{fileName}_{DateTime.Now:yyyyMMddHHmmss}.xlsx"); } private object GetPropertyValue(object obj, string propertyName) { // 使用反射或预编译的表达式树获取属性值(性能考虑可用缓存) return obj.GetType().GetProperty(propertyName)?.GetValue(obj, null); } private void SetCellValue(ICell cell, object value) { if (value == null) cell.SetCellValue(""); else if (value is DateTime) cell.SetCellValue(((DateTime)value).ToString("yyyy-MM-dd HH:mm:ss")); else if (value is bool) cell.SetCellValue((bool)value ? "是" : "否"); else if (IsNumeric(value)) cell.SetCellValue(Convert.ToDouble(value)); else cell.SetCellValue(value.ToString()); } private bool IsNumeric(object value) { /* 判断是否为数字类型 */ } } // 列映射配置类 public class ColumnMapping { public string HeaderText { get; set; } // Excel列头,如“用户姓名” public string PropertyName { get; set; } // 实体属性名,如“UserName” public Func<object, object> Formatter { get; set; } // 格式化委托,如 value => ((int)value) == 1 ? "启用" : "禁用" }

4.3 前后端协作:查询与导出的参数一致性

这是确保导出数据与列表显示数据一致的关键。前端exportData方法构建的参数对象(searchParams),必须与后端loadPageData方法接收的参数结构完全一致。这样,后端可以用同一套查询逻辑(或Service方法)来获取数据,无论是用于分页展示还是全量导出。

// UserController.cs public class UserController : BaseController { public ActionResult GetPageList(UserSearchModel search, int pageIndex = 1, int pageSize = 20) { // 分页查询逻辑 var pagedList = _userService.GetPagedList(search, pageIndex, pageSize); return Json(new { success = true, data = pagedList }); } [HttpPost] // 注意这里是Post public ActionResult ExportUserList(UserSearchModel search) { // 使用相同的SearchModel和Service方法,但获取全部数据 var allData = _userService.GetList(search); // 这个方法内部可能忽略分页参数 // 定义导出列映射 var columns = new List<ColumnMapping> { new ColumnMapping { HeaderText = "用户ID", PropertyName = "Id" }, new ColumnMapping { HeaderText = "用户名", PropertyName = "UserName" }, new ColumnMapping { HeaderText = "状态", PropertyName = "Status", Formatter = v => ((int)v) == 1 ? "正常" : "冻结" }, new ColumnMapping { HeaderText = "创建时间", PropertyName = "CreateTime", Formatter = v => ((DateTime)v).ToString("yyyy-MM-dd") } }; return ExportToExcel(allData, columns, "用户列表"); } }

重要经验:后端的ExportUserListAction应该使用[HttpPost]特性,并与前端的表单提交方式匹配。查询模型UserSearchModel可以同时用于GetPageListExportUserList,确保查询条件完全一致。

5. 实战应用:快速创建一个用户管理页面

理论讲完,我们来点实际的。假设要创建一个用户查询列表页,支持按用户名、状态筛选,分页展示,并导出Excel。

第一步:创建具体的UserViewModel

// UserViewModel.js function UserViewModel() { // 调用父类构造函数,传入API地址 BasePagingViewModel.call(this, { dataLoadUrl: '/User/GetPageList', exportUrl: '/User/ExportUserList', defaultPageSize: 15 }); var self = this; // 扩展具体的查询条件 self.searchParams().userName = ko.observable(''); self.searchParams().status = ko.observable(''); // ''表示全部,1正常,0冻结 // 定义EasyUI Datagrid的列配置 self.gridOptions = { columns: [[ { field: 'id', checkbox: true }, { field: 'userName', title: '用户名', width: 100 }, { field: 'realName', title: '真实姓名', width: 100 }, { field: 'status', title: '状态', width: 60, formatter: function(value) { return value == 1 ? '正常' : '冻结'; } }, { field: 'createTime', title: '创建时间', width: 120, formatter: function(value) { return new Date(value).toLocaleString(); } }, { field: 'action', title: '操作', width: 80, formatter: function(value, row) { return '<a href="javascript:void(0)" onclick="viewModel.editUser(' + row.id + ')">编辑</a>'; }} ]], pagination: true, rownumbers: true, fitColumns: true, singleSelect: false, toolbar: '#toolbar' // 指向页面上的工具栏div }; // 子类特有的方法 self.editUser = function(id) { // 打开编辑对话框... }; } // 继承原型链 UserViewModel.prototype = Object.create(BasePagingViewModel.prototype); UserViewModel.prototype.constructor = UserViewModel; // 页面加载完成后初始化 $(function() { window.viewModel = new UserViewModel(); ko.applyBindings(viewModel, document.getElementById('mainContainer')); // 初始化加载第一页数据 viewModel.loadPageData(); });

第二步:创建HTML视图

<div id="mainContainer"> <!-- 工具栏 --> <div id="toolbar" style="padding:5px;"> <span>用户名:</span> <input class="easyui-textbox">BasePagingViewModel.prototype.debouncedSearch = ko.observable(null); // 在构造函数中初始化 // 在构造函数或初始化方法中创建防抖函数 self.debouncedSearch = ko.computed(function() { return _.debounce(self.search.bind(self), 500); // 使用lodash或underscore的debounce }).extend({ deferred: true }); // 使用deferred computed确保只计算一次 // 在搜索框的data-bind中:event: { input: debouncedSearch }

问题:切换页面再切回来时,重复加载相同数据。解决方案:为loadPageData方法增加简单的缓存机制,缓存键由查询参数和页码哈希生成。

BasePagingViewModel.prototype._cache = {}; BasePagingViewModel.prototype.loadPageData = function(page) { var self = this; var cacheKey = JSON.stringify(ko.toJS(self.searchParams)) + '_' + page + '_' + self.pageSize(); if (self._cache[cacheKey] && !self.forceReload) { var cached = self._cache[cacheKey]; self.dataList(cached.list); self.totalCount(cached.total); self.currentPage(page); return; // 直接使用缓存 } // ... 原有的Ajax请求逻辑,在success回调中缓存结果 // success: function(result) { // self._cache[cacheKey] = { list: result.data.list, total: result.data.total }; // ... // } }; // 提供一个清除缓存的方法,在主动刷新或条件变更时调用 self.clearCache = function() { self._cache = {}; };

6.2 复杂查询条件的处理

问题:查询条件包含日期范围、多选等复杂类型,ko.toJS转换后格式不符合后端要求。解决方案:在loadPageDataexportData方法中,对参数进行预处理。

BasePagingViewModel.prototype.buildRequestParams = function() { var params = ko.toJS(this.searchParams); // 处理日期范围:假设前端是{startDate: obs, endDate: obs},后端需要两个独立参数 if (params.dateRange && params.dateRange.start && params.dateRange.end) { params.startDate = params.dateRange.start; params.endDate = params.dateRange.end; delete params.dateRange; } // 处理多选数组:确保是逗号分隔的字符串(如果后端接口这样要求) if (params.statusList && Array.isArray(params.statusList)) { params.status = params.statusList.join(','); delete params.statusList; } return params; }; // 然后在loadPageData中使用this.buildRequestParams()构建参数

6.3 EasyUI Datagrid与KnockoutJS绑定的深度集成问题

坑点一:动态列(如权限控制的列显示/隐藏)EasyUI Datagrid的列配置在初始化后,直接通过KnockoutJS更新gridOptions.columns可能不会生效。解决方案:使用Datagrid的column方法动态修改。

// 在viewModel中定义一个方法 self.toggleColumn = function(field, visible) { var grid = $('#你的datagridID'); var col = grid.datagrid('getColumnOption', field); if (col) { col.hidden = !visible; grid.datagrid(); } }; // 或者,在列定义中使用`hidden`属性绑定一个ko.observable,并在自定义绑定handler的update部分处理

坑点二:行样式与格式化函数KnockoutJS的绑定主要处理数据,而EasyUI的rowStylerformatter是渲染时执行的函数。如果样式或格式化依赖viewModel中的其他可观察属性,需要确保这些函数能访问到最新的viewModel实例。解决方案:在格式化函数中,通过闭包或全局变量引用viewModel

// 在定义gridOptions时 formatter: function(value, row, index) { // 确保viewModel在作用域内 if (window.viewModel && window.viewModel.someCondition()) { return '<span style="color:red">' + value + '</span>'; } return value; }

6.4 导出功能的扩展:多格式与大数据量

需求一:支持PDF导出后端逻辑类似,但使用iTextSharp等PDF库。前端exportData方法传入exportType: 'pdf',后端根据此参数选择不同的导出方法和ContentType。

需求二:导出超大数据量(百万行)直接查询所有数据到内存再导出会导致内存溢出和超时。解决方案:采用分页流式导出。

  1. 前端触发导出后,后端立即响应,开始一个后台任务或生成一个任务ID。
  2. 后端使用yield return或分页查询,将数据分批写入Excel(NPOI支持流式写入)。
  3. 前端轮询任务状态,完成后提供下载链接。或者,对于MVC,可以尝试使用HttpResponse.Flush()进行流式输出,但复杂度较高。更常见的做法是生成文件到服务器临时目录,然后提供下载链接。

一个简化的后端流式导出思路:

public ActionResult ExportLargeData(UserSearchModel search) { Response.Buffer = false; Response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; Response.AddHeader("Content-Disposition", "attachment; filename=large_export.xlsx"); using (var workbook = new XSSFWorkbook()) { var sheet = workbook.CreateSheet("Data"); // ... 写标题 int rowNum = 1; int pageIndex = 1; const int pageSize = 5000; List<User> dataChunk; do { dataChunk = _userService.GetPagedList(search, pageIndex, pageSize).ToList(); foreach (var item in dataChunk) { // ... 写一行数据 rowNum++; } // 将当前部分数据写入响应流 using (var ms = new MemoryStream()) { workbook.Write(ms); Response.BinaryWrite(ms.ToArray()); Response.Flush(); } // 清理已写入的数据行,避免内存增长(注意:NPOI的Sheet行对象可能无法直接清除,此方案需谨慎测试) // 更稳健的方案是使用专门的流式Excel写入库,如`SpreadsheetLight`或`ExcelDataReader`的写入器。 pageIndex++; } while (dataChunk.Count == pageSize); } return new EmptyResult(); }

注意:流式导出对技术和服务器配置要求较高,需要仔细测试。对于超大数据量,更推荐使用后台任务生成文件+前端下载的模式。

7. 总结与展望:模式的普适性思考

回顾整个方案,其核心价值在于将可变与不变分离。不变的,是分页、查询、绑定、导出这个工作流和与之对应的状态、行为。可变的,是具体的查询字段、表格列、数据接口和导出格式。通过一个精心设计的BasePagingViewModel基类,我们封装了所有不变的部分,并通过继承、组合和配置,优雅地支持了可变的部分。

这个模式不仅适用于EasyUI+KnockoutJS+MVC。其思想可以迁移到其他技术栈:

  • Vue.js + Element UI:你可以创建一个BasePagingMixin混入或一个抽象的BaseTable组件,管理current-pagepage-sizetableDataloading状态,以及handleSearchhandleResethandleExport方法。
  • React + Ant Design:可以构建一个自定义Hook,如usePagingTable,返回分页状态、表格数据源、加载状态以及一系列操作方法。
  • 纯后端API设计:可以定义一个统一的PageRequest(包含页码、页大小、排序、通用查询条件)和PageResult<T>响应体,所有分页查询接口都遵循此规范。

即使在今天,虽然EasyUI、KnockoutJS、MVC 4.0已不能算是最前沿的技术,但它们在无数遗留系统中依然稳定运行。为这样的系统引入一个清晰、可复用的架构模式,远比盲目重写更能创造价值。这个“一个共通的viewModel”方案,正是这种价值的体现——它用最小的改动,解决了开发中最痛的重复问题,让老项目也能焕发出整洁代码的活力。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/19 22:26:35

广州微闻网络AI落地技术实践:Agent定制、Token供应与云计算全栈技术解析

文聚焦广州微闻网络科技有限公司在AI落地领域的技术实践&#xff0c;深入解析Agent定制开发、Token聚合供应、云计算全栈服务三大技术方向的具体实现方案&#xff0c;并涵盖AI提效、AI转型培训、WorkBuddy使用培训等业务的技术能力&#xff0c;为技术团队提供参考。一、公司技术…

作者头像 李华
网站建设 2026/8/19 22:24:38

在线教育平台开课前三网验收:视频域、直播与 API

在线教育平台开课前三网验收&#xff1a;视频域、直播与 API工具地址&#xff1a;https://www.speedce.com 社区论坛&#xff1a;https://bbs.speedce.com 联系&#xff1a;speedceadsgmail.com写在前面 开课铃响了你才发现视频域移动红——太晚了。 本文是一份围绕「在线教育平…

作者头像 李华
网站建设 2026/8/19 22:24:26

多个人同时提问但位置有限

摘要&#xff1a;在算力昂贵的大模型&#xff08;LLM&#xff09;推理与高并发后端架构中&#xff0c;资源的物理约束是不可跨越的红线。假设系统只有 3 个 GPU 槽位&#xff08;Inference Slots / Worker Threads&#xff09;&#xff0c;当 10 个并发请求同时涌入时&#xff…

作者头像 李华
网站建设 2026/8/19 22:22:02

UnrealPakViewer 完整上手教程:三步摸清任意 UE4 Pak 文件内部结构

UnrealPakViewer 完整上手教程&#xff1a;三步摸清任意 UE4 Pak 文件内部结构 【免费下载链接】UnrealPakViewer 查看 UE4 Pak 文件的图形化工具&#xff0c;支持 UE4 pak/ucas 文件 项目地址: https://gitcode.com/gh_mirrors/un/UnrealPakViewer 遇到打好的 UE4 工程…

作者头像 李华
网站建设 2026/8/19 22:17:39

102.环形缓冲区之读指针与写指针:原理、实现与完整代码

一、环形缓冲区的核心概念环形缓冲区本质上是一个首尾相连的数组&#xff0c;通过两个关键指针实现数据的读写管理&#xff1a;写指针&#xff08;pw&#xff09;&#xff1a;标记下一个数据写入的位置读指针&#xff08;pr&#xff09;&#xff1a;标记下一个数据读取的位置这…

作者头像 李华