用 TypeScript 写一个轻量级的 UI 框架之十一:列表组件综述

认识列表

我们创建 Web 程序,其中一个主要的任务便是数据的处理。程序数据这里指的是业务相关的数据实体开发:从最开始的数据库表设计,到后台业务的相关处理(提交数据的反序列化、表单验证、业务逻辑等),再到前端配合 UI 方面的设计,具体如表单负来责数据的输入,紧接着通过列表、表格和图表(charts)等的展示手段呈现数据……等等。业务层面的逻辑处理固然非常重要,不过今天这里我们侧重谈的是 UI 层面的展示。大量结构化数据的一种主要输出形式就是列表,——本文就打算介绍列表组件的设计与使用。需要指出的是,当前涉及的列表与表格,它们之间的界限并不明显,列表同样可为展示二维式的数据所服务。

关注点分离

虽说当前我们制作的目标是一个 UI 组件,但本文却不太考虑前端呈现的方式或具体的交互效果,也就是说,MVC 分离中的 View 部分,我们这里不会着墨过多。须知道,五彩缤纷的 UI 效果各有实现,不一而足,不同的项目有不同的需求,很难在一套代码中抽象之。所以那些 CSS/JS 效果就不讨论了,当前 AJAXJS 库仅满足于几个常见的场景,然后更关心的是围绕如何定义数据和读取数据这些基本问题。相比之下,这部分的代码会简单得多。

本文完整源码在:https://gitee.com/sp42_admin/ajaxjs/blob/master/aj-ts/src/list/list.ts

实体定义

基础实体 BaseObject

今时今日,我们在前端 TypeScript 中也要为业务实体建模(既然强调“强类型”)。我们认为每一种数据类型,至少具备有下面两个字段:idname,对应声明类型如下 BaseObject

/**
 * 实体,一般至少有 id 和 name 字段
 */
declare interface BaseObject {
    /**
     * 实体 id
     */
    id: number;

    /**
     * 实体名称
     */
    name: string;
}

但请不要认为这是对实体的一种强制的约束,“必须要求后端提供每一个数据都带上这两个字段”——这是否定的。对于前端来说,仅仅只能算是“语义”上的弱约束,即使后端不完全提供也没关系。

而对于后台接口所返回的数据实体,如何定义结构——那才是强制的约束。

定义接口数据 PageListRepsonseResult

列表 UI 显然是一个 AJAX 程序,是AJAX 程序就会返回一个消息体。故我们定义一个后端响应的消息体。之所以那样子做,是为了框架内有一个统一抽象的基本结构,从而消除不必要的重复。但凡遇到一个数据返回,不管它还有其他的什么字段,而最起码的就应该有以下字段。

/**
 * 后端响应的消息
 */
declare interface RepsonseResult {
    /**
     * 操作代码
     */
    code?: number;

    /**
     * 操作是否成功
     */
    isOk: boolean;

    /**
     * 操作说明
     */
    msg: string;

    /**
     * 结果,实体,可以是任意类型
     */
    result: BaseObject[];

    /**
     * 进行新建的时候返回的实体 id
     */
    newlyId?: number
}

如果是 JSON 格式消息体,那么须返回如 {isOk: true, msg: "xxxxx"} 那样才符合 RepsonseResult 对象字段名称及字段类型之要求。显然此处就是一种强制要求。如果你的实际项目不是返回那些字段,那么就要修改框架里面这个 RepsonseResult 接口定义了。

我们的目的就是提供一种设计框架的思路,故你具体的项目还是得按具体的去办,结合实际情况修改——实际上即使要进行修改,相信也是大同小异的。

结合列表组件,自然有分页的数据返回,因此我们定义一个 PageListRepsonseResult,它仅仅比其父类多出一个字段:total

/**
 * 分页列表专用的结果数据
 */
interface PageListRepsonseResult extends RepsonseResult {
	/**
	 * 结果总数
	 */
	total: number;
}

如果是图片上传,那么还可以有专门图片上传后返回的结果 ImgUploadRepsonseResult……,总之就是不断扩展基类。

/**
 * 图片上传响应的消息
 */
declare interface ImgUploadRepsonseResult extends RepsonseResult {
    /**
     * 相对地址
     */
    imgUrl: string;

    /**
     * 图片绝对地址,http 开头的
     */
    fullUrl: string;

    /**
     * 图片列表
     */
    pics: string[];
}

数据容器 DataStore

回到当前列表数据的话题中。虽然有了 PageListRepsonseResult 结果,但是我们把这个返回的数据放到另外一个对象的容器中,即 PageListRepsonseResult.result: BaseObject[] 对应 DataStore.result: BaseObject[]。DataStore 就是客户端本地的数据容器。DataStore 继承于 Ajax 接口,Ajax 原本也有 result: any 字段,现在 DataStore 把它覆盖了,更明确了类型为 BaseObject[] 而不是通用的 any 类型。除此之外,来自 PageListRepsonseResult 的 total 总数也会映射到 DataStore 的 total 字段中,供分页所用。

顺便看看 Ajax 接口。打造这个接口之目的是统一规范凡是用到 XHR 请求的地方。

/**
 * 对象包含 RPC 请求
 */
declare interface Ajax {
    /**
     * 接口地址
     */
    apiUrl: string;

    /**
     * 真实发送的请求,可能包含 QueryString
     */
    realApiUrl?: string;

    /**
     * 每次请求都附带的参数,一经修改就不可修改的
     */
    baseParam?: JsonParam;

    /**
     *  与 baseParam 合并后每次请求可发送的,可以修改的
     */
    extraParam?: JsonParam;

    /**
     * 上次发送的请求
     */
    lastRequestParam?: JsonParam;

    /**
     * 请求结果,可以是任意类型
     */
    result?: any;

    /**
     * 是否自动加载数据
     */
    isAutoLoad: boolean;

    /**
     * 请求 GET 数据
     */
    getData(): void;

    /**
     * 得到数据后的回调
     */
    onLoad?: (j: RepsonseResult) => void;
}

控制分页的字段

继续为 DataStore 添砖加瓦,扩充相关的字段。这不,我们还缺少分页相关的字段,于是修改 DataStore 如下。

/**
 * 列表数据
 */
interface DataStore extends Ajax {
	/**
	 * 是否分页,false=读取所有数据
	 */
	isPage: boolean;

	pageStart: number;

	pageSize: number;

	initPageSize: number;

	total: number;

	/**
	 * 请求结果
	 */
	result: BaseObject[];

	totalPage: number;

	currentPage: number;

	/**
	 * 默认的分页参数其名字
	 */
	pageParamNames: string[];
}

分页规则采用 MySQL 方式的 start/limit 方式。你可以在 pageParamNames 指定参数名称,否则默认就是 startlimit。当然了如果列表不需要分页,那么设置组件属性 :is-page="false" 即可。

DataStore 数据仓库

如此这般我们的数据仓库 DataStore 就成型了,下面接着写它的实现。

export var datastore = {
	props: {
		apiUrl: { type: String, required: true },                       // JSON 接口地址
		isPage: { type: Boolean, default: true },
		initPageSize: { type: Number, required: false, default: 9 },
		isAutoLoad: { type: Boolean, default: true },
		baseParam: { type: Object, default() { return {}; } },
		pageParamNames: { type: Array, default() { return ['start', 'limit']; } }, 	// 默认的分页参数其名字
		onLoad: Function
	},
	data(this: DataStore) {
		return {
			result: [],
			extraParam: {},	                // 与 baseParam 合并后每次请求可发送的,可以修改的
			pageSize: this.initPageSize,
			total: 0,
			totalPage: 0,
			pageStart: 0,
			currentPage: 0
		};
	},

	methods: {
		/**
		 * 分页,跳到第几页,下拉控件传入指定的页码
		 * 
		 * @param this 
		 * @param ev 
		 */
		jumpPageBySelect(this: DataStore, ev: Event): void {
			let selectEl: HTMLSelectElement = <HTMLSelectElement>ev.target;
			let currentPage: string = selectEl.options[selectEl.selectedIndex].value;

			this.pageStart = (Number(currentPage) - 1) * this.pageSize;
			this.getData();
		},

		/**
		 * PageSize 改变时候重新分页
		 * 
		 * @param this 
		 * @param ev 
		 */
		onPageSizeChange(this: DataStore, ev: Event): void {
			this.pageSize = Number((<HTMLInputElement>ev.target).value);
			this.count();
			this.getData();
		},

		count(this: DataStore): void {
			let totalPage: number = this.total / this.pageSize, yushu: number = this.total % this.pageSize;
			this.totalPage = parseInt(String(yushu == 0 ? totalPage : totalPage + 1));
			//@ts-ignore
			this.currentPage = parseInt((this.pageStart / this.pageSize) + 1);
		},

		/**
		 * 前一页
		 * 
		 * @param this 
		 */
		previousPage(this: DataStore): void {
			this.pageStart -= this.pageSize;
			//@ts-ignore
			this.currentPage = parseInt((this.pageStart / this.pageSize) + 1);

			this.getData();
		},

		/**
		 * 下一页
		 * 
		 * @param this
		 */
		nextPage(this: DataStore): void {
			this.pageStart += this.pageSize;
			this.currentPage = (this.pageStart / this.pageSize) + 1;

			this.getData();
		}
	}
};

DataStore 不是一个 Vue 组件,而是作为 mixins 组合到组件中去,所以一般情况下不单独使用 DataStore。

列表组件

列表组件 list 自然是继承 DataStore,拥有它全部的特性,然后就有自己跟 UI 方面更多的考量。

/**
 * 列表控件
 */
interface List extends DataStore, Vue {
	/**
	 * 数据分页是否追加模式,默认不追加 = false。 App 一般采用追加模式
	 */
	isDataAppend: boolean;

	/**
	 * 到达底部是否自动加载下一页,通常在 移动端使用,这个应该是元素的 CSS Selector
	 */
	autoLoadWhenReachedBottom: boolean;
}

具体源码就不张贴了,参见源码仓库。

整理 aj-list 组件属性(props)如下。

属性类型说明是否必填默认值
api-urlString接口地址yn/a
is-pageBoolean是否分页,false=读取所有数据nn/a
init-page-sizeNumber初始的分页笔数n9
is-auto-loadBoolean是否一渲染 UI 之后就自动加载数据ntrue
base-paramObject初始参数。每次请求都附带的参数,一经声明创建就不可修改的n{}
page-param-namesString[]默认的分页参数其名字n['start', 'limit']
show-default-uiBoolean如果只是单纯作为分页组件,那么则不需要 UIntrue
is-show-footerBoolean是否显示分页 UIntrue
href-strString链接nn/a
isDataAppendBoolean数据分页是否追加模式,默认不追加 = false。 App 一般采用追加模式nfalse
auto-load-when-reached-bottomString到达底部是否自动加载下一页,通常在 移动端使用,这个应该是元素的 CSS Selectornn/a
on-loadFunction加载完毕的回调函数nn/a

基础 UI

UI 实现方面可以完全由用户自定义。不过我们 CSS 库也自带了几种基础样式。

最简单的 <ul><li> 列表,例子这里

在这里插入图片描述

图文样式列表,例子这里

在这里插入图片描述
宫格样式列表,例子这里

在这里插入图片描述

相关推荐
<p> <span style="font-size:14px;color:#E53333;">限时福利1</span><span style="font-size:14px;">购课进答疑群专享柳峰(刘运强)老师答疑服务</span> </p> <p> <br /> </p> <p> <br /> </p> <p> <span style="font-size:14px;"></span> </p> <p> <span style="font-size:14px;color:#337FE5;"><strong>为什么需要掌握高性能MySQL实战?</strong></span> </p> <p> <span><span style="font-size:14px;"><br /> </span></span> <span style="font-size:14px;">由于互联网产品户量大、高并发请求场景多,因此对MySQL性能、可性、扩展性都提出了很高要求。使MySQL解决大量数据以及高并发请求已经是程序员必备技能,也是衡量一个程序员能力和薪资标准一。</span> </p> <p> <br /> </p> <p> <span style="font-size:14px;">为了让大家快速系统了解高性能MySQL核心知识全貌,我为你总结了</span><span style="font-size:14px;">「高性能 MySQL 知识框架图」</span><span style="font-size:14px;">,帮你梳理学习重点,建议收藏!</span> </p> <p> <br /> </p> <p> <img alt="" src="https://img-bss.csdnimg.cn/202006031401338860.png" /> </p> <p> <br /> </p> <p> <span style="font-size:14px;color:#337FE5;"><strong>【课程设计】</strong></span> </p> <p> <span style="font-size:14px;"><br /> </span> </p> <p> <span style="font-size:14px;">课程分为四大篇章,将为你建立完整 MySQL 知识体系,同时将重点讲解 MySQL 底层运行原理、数据库性能调优、高并发、海量业务处理、面试解析等。</span> </p> <p> <span style="font-size:14px;"><br /> </span> </p> <p> <span style="font-size:14px;"></span> </p> <p style="text-align:justify;"> <span style="font-size:14px;"><strong>一、性能优化篇</strong></span> </p> <p style="text-align:justify;"> <span style="font-size:14px;">主要包括经典 MySQL 问题剖析、索引底层原理和事务与锁机制。通过深入理解 MySQL 索引结构 B+Tree ,学员能够从根本上弄懂为什么有些 SQL 走索引、有些不走索引,从而彻底掌握索引使和优化技巧,能够避开很多实战中遇到“坑”。</span> </p> <p style="text-align:justify;"> <br /> </p> <p style="text-align:justify;"> <span style="font-size:14px;"><strong>二、MySQL 8.0新特性篇</strong></span> </p> <p style="text-align:justify;"> <span style="font-size:14px;">主要包括窗口函数和通表表达式。企业中许多报表统计需求,如果不采窗口函数,普通 SQL 语句是很难实现。</span> </p> <p style="text-align:justify;"> <br /> </p> <p style="text-align:justify;"> <span style="font-size:14px;"><strong>三、高性能架构篇</strong></span> </p> <p style="text-align:justify;"> <span style="font-size:14px;">主要包括主从复制和读分离。在企业生产环境中,很少采单台MySQL节点情况,因为一旦单个节点发生故障,整个系统都不可,后果往往不堪设想,因此掌握高可架构实现是非常有必要。</span> </p> <p style="text-align:justify;"> <br /> </p> <p style="text-align:justify;"> <span style="font-size:14px;"><strong>四、面试篇</strong></span> </p> <p style="text-align:justify;"> <span style="font-size:14px;">程序员获得工作第一步,就是高效准备面试,面试篇主要从知识点回顾总结角度出发,结合程序员面试高频MySQL问题精讲精练,帮助程序员吊打面试官,获得心仪工作机会。</span> </p>
©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页