65.9K
CodeProject 正在变化。 阅读更多。
Home

用于单页应用程序中 CRUD 操作的 SLGrid Knockout 组件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.40/5 (5投票s)

2015 年 11 月 28 日

CPOL

4分钟阅读

viewsIcon

17776

Knockout 组件 SLGrid。

引言

SLGrid 是一个具有标准网格功能的 Knockout 组件:

  • 分页
  • 排序
  • 过滤
  • 内联编辑
  • 每个单元格都可以包含另一个 Knockout 组件,例如日期选择器、下拉列表、日历或任何其他可重用组件
  • ...

我将介绍 SLGrid 组件在单页应用程序环境中用于 CRUD 操作的用法。
这些网格功能对于拥有许多不同表和关系网站的管理部分非常有用。
实际上,这是为了最终确定对某个实体进行 **C**reate(创建)、**R**ead(读取)、**U**pdate(更新)和 **D**elete(删除)所需的操作集。

背景

SLGrid 是客户端组件网格,而不是 ASP.NET 5.1、MVC 4.2、PHP 7.8 等的网格。

我决定遵循 Steve 的 Sanderson 的示例,使用 'yeoman' 来脚手架一个包含 Knockout、CrossroadsJS 和 RequireJS 的 SPA。CrossroadsJS 实现哈希路由,RequireJS 处理模块加载和依赖项。

我正在遵循的演示是
Steve Sanderson - 使用 Knockout 构建大型单页应用程序的架构

生成 Knockout 应用程序的起点

启动 Knockout 项目比预期的要容易。
以下是安装 'Yeoman' 并脚手架项目的命令

   npm install -g yo
   npm install -g generator-ko
   yo ko

这给了我们一个简单的项目结构,包含路由和两个初始视图(主页和关于页面)。
可以手动添加其他视图和组件,但 Yeoman 也提供了一个快捷命令

   yo ko:component <name>

此演示中使用的关键技术是

  • Knockout 组件,一种将 UI 代码组织到自包含、可重用块中的简洁方式
  • Reguirejs,用于异步模块定义 (AMD)
  • Knockout Validation
  • gulpjs,流式构建系统
  • babel,JavaScript ES2015 转译器
  • JavaScript 继承模式
  • jQuery 用于管理对服务的 Ajax 调用
  • Bootstrap 用于 HTML 标记的样式化

试试看

使用代码

对于实体的 CRUD 操作,我们需要定义 3 个 JavaScript 类,实际上是 3 个 AMD 模块。以实体 'People' 为例,我们定义

  • Person,继承 `SLEntity`,定义具有默认值和验证规则的属性。
  • PersonDB,继承 `SLEntityDB`,数据库适配器,针对不同类型的数据库有所不同,执行与服务器的 ajax 调用
  • PersonGrid,使用 `SLGrid` 和 `SLPager`,维护实体列表,执行网格操作,如分页、排序、内联编辑... 它定义了将在表格视图中显示的 Person 实体的属性。它根据列的定义生成行和行编辑模板。

实际上,我们可以使用 'yeoman' 生成这些类,从数据库定义读取并尊重数据库的所有关系,例如:一对多,多对多...

// Person
class Person extends SLEntity {
    constructor(data) {
        super(data);

        this.PersonId = ko.observable(data.PersonId || 0)
        this.Name = ko.observable(data.Name || "").extend({ required: true, minLength: 2 })
        this.IsOnTwitter = ko.observable(data.IsOnTwitter === undefined ? false : data.IsOnTwitter)
        this.TwitterName = ko.observable(data.TwitterName || "")
        this.City = new CityAsForeign(data.City || {})
        this.NumOfPosts = ko.observable(data.NumOfPosts || 0)
        this.LastLogin = ko.observable(data.LastLogin ? new Date(data.LastLogin) : new Date())
    }
}

Person 源代码

// PersonDB

class PersonDB extends SLEntityDB {
    constructor(data) {
        super(data);
    }
}

PersonDB.prototype.Url = {
    "get" :     "api/people",
    "getById":  "api/people/getById",
    "add":      "api/people/post",
    "update":   "api/people/put"
}

PersonDB 源代码

// PersonGrid

class PersonGrid {

  constructor(params) {
    var self = this;

    this.Person = Person;
    this.pageSize = ko.observable(params.pageSize);

    this.pager = ko.observable();
    this.grid = ko.observable();
    this.grid.subscribe(function (grid) {
        // now we have the grid component instantiated
    })

    this.columns = [
        { fieldName: 'PersonId', header: 'Id', width: '50px', align: 'right' },
        { fieldName: 'Name', header: 'Name', width: 'auto' }, 
        { fieldName: 'IsOnTwitter', header: 'Twitter', width: '70px', align: 'center', presentation: 'bindingHandlerCheckbox' },
        { fieldName: 'City', header: 'City', width: '140px', 
                    markup: "<bs-select params='entityAsForeign:City, isViewMode:isViewMode' />" },
        { fieldName: 'NumOfPosts', header: '#Posts', width: '70px', align: 'center' },
        { fieldName: 'LastLogin', header: 'Last Login', width: '180px', align: 'center', 
                    markup: "<sl-date-picker params='date:LastLogin, isViewMode:isViewMode'/>" },
        { fieldName: '', header: 'Inline', width: '80px', presentation: 'bindingHandlerEditInline', sortable: false, align: 'center' },
        { fieldName: '', header: 'Edit', width: '60px', presentation: 'bindingHandlerEdit', sortable: false, align: 'center' }
    ];

    this.filter = new Person({});
    this.filter.City.CityId.subscribe(function (newValue) {
        self.grid()
            .Filter({ CityId: newValue == 'CAPTION' ? 0 : newValue })
            .getItems();
    });

    this.pageSize.subscribe(function (newValue) {
        self.grid().getItems(1 /*page*/, newValue /*pageSize*/);
    });

    // Add
    this.addNew = function () {
        location.hash = '#person-add';
    }

    // Edit
    this.editEntity = function (vm, e) {
        var personDB = Person.prototype.entityDB;
        personDB.getById(vm.PersonId(), function (data) {
            window.personData = data;
            location.hash = '#person-edit/' + vm.PersonId();
        })
    }

  }
}

PersonGrid 源代码

基础 JavaScript 类可以完成这项工作
  SLEntity 源代码
  SLEntityDB 源代码

关于 $(document).ready

在此 SPA 环境中,我们有动态加载页面,按需加载,并且到处都有组件:页面是 Kncokout 组件,网格是 Knockout 组件,下拉列表是 Knockout 组件... 我们可以使用 requirejs/domReady 插件,但由于 Knockout,我们有了更好的解决方案。我们希望在绑定过程中针对组件的元素运行代码,因此自定义绑定是可行的方法。

我们将元素放入模板中

<div data-bind="MyComponentHandler:true">

我们像这样设置 ViewModel

// Binding will be applied when the element is introduced to the document.

    ko.bindingHandlers.MyComponentHandler = {
        init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
            viewModel.myElem = $(element);
            // bind any plugin here
            viewModel.myElem.selectpicker({ size: 10 });                
        }
    }

    class MyComponent {
        constructor(params) {
            this.myElem = null;
            // ...
        }
    }

子 Knockout 组件如何通信

组件是组织 UI 代码到自包含、可重用块中的强大、简洁的方式。
Knockout 组件通过传递给组件的 'params' 对象进行通信。
通常这是一个包含多个参数的键值对象,并且通常由组件的 ViewModel 构造函数接收。

这张图展示了Filter (下拉列表)和SLGrid 通信的方式

子 Knockout 组件 SLGrid 和 SLPager 如何连接

模拟器

mockjax 插件是一个开发和测试工具,用于拦截和模拟使用 jQuery 发出的 ajax 请求,对生产代码的更改影响最小。对于这个示例应用程序,服务器实际上是一个模拟服务器,它使我们能够进行实时演示,而无需在任何地方托管实际服务器。

打包和缩小

组件的动态加载。我们将 People 所需的组件放入同一个包中

   bundles: {
        //...
        'people': [ 'components/person-grid/person-grid', 
                    'pages/people/people', 
                    'pages/people/add/person-add', 
                    'pages/people/edit/person-edit']
    }

这样,只有当用户点击顶部菜单栏中的 'People' 时,我们才能动态加载 'people.js'。

People on demand

SLGrid 的 Yeoman 生成器

计划使用以下工具为 SLGrid 创建一个生成器:
https://yeoman.node.org.cn/authoring/

生成器将读取任何数据库定义

  • 关系

为某个表(实体)的 CRUD 操作生成所需的页面和组件。
这样,我们就可以为包含许多表的某个管理站点生成完整的起点。
通过精心设计的组件和 JavaScript 类,开发人员将更容易自定义模板和实现附加逻辑。

例如,读取 City 表的定义,我们可以生成 City

class City extends SLentity 
{
    constructor(data) {
        super(data);

        this.CityId = ko.observable(data.CityId)
                           .extend({ defaultValue: 102, required: true, minLength: 2 })
        this.Name = ko.observable(data.Name)
        
        this.CountryId = ko.observable(data.CountryId).extend({ foreignKey: Country })
    }
}

此外,'yeoman' 还生成了 City 的 CRUD 操作所需的其他文件。

  •  服务器上的 CityController 用于操作
     - getItems
     - getItemById
     - Add
     - Update
     - Remove
  •  CityDto - 客户端和服务器之间的数据传输对象

components/
   city-grid/
       city-grid.js
       city-grid.html

models/
   city/
      city.js
      city-db.js

pages/    
    cities/
       add/
           city-add.js
           city-add.html

        edit/
           city-edit.js
           city-edit.html

       cities.js
       cities.html

© . All rights reserved.