由移动端级联选择器所引发的对于数据结构的思考
前言
最近在做移动端项目的时候遇到了省市区选择的功能。以往做项目时都是省市区分开的下拉框样式。这次希望实现效果图要求的级联选择器。我是 Framework7 框架的忠实粉丝,庆幸的是 Framework7 已经有模拟 iOS 选择框效果的 Picker 组件。在开发之前我先搜索了现有的一些选择器插件,整体而言都能满足需求但都不完美,比如滑动不流畅、显示有 Bug 等等。
Picker 级联选择器
基于 Framework7 制作级联选择器比较简单,关键是生成省市区数组以及省市区之间的联动。
以下是 CityPicker 的基本参数设置:
var pickerLocation = myApp.picker({ input: '#location',//选择器 rotateEffect: true,//设置旋转效果 toolbarTemplate: '',//自定义按钮 cols: [{ cssClass: 'f-s-14',//添加自定义类 width: 100,//列宽 textAlign: 'left',//对齐方式 values: province,//省数组 onChange: function(picker, province) {//联动方法 } }, { cssClass: 'f-s-14',//添加自定义类 width: 100,//列宽 textAlign: 'center',//对齐方式 values: city,//市数组 onChange: function(picker, city) {//联动方法 } }, { cssClass: 'f-s-14',//添加自定义类 width: 100,//列宽 textAlign: 'right',//对齐方式 values: area,//区数组 } ] });
其中省市区的格式都是基本数组,所以必须循环省市区数据生成相应的数组或者数据本身具有可以直接获取数组的结构。
province = ['北京','天津','河北','山东',...] city = ['济南','青岛','淄博','滨州',...] area = ['滨城区','惠民县','阳信县','博兴县',...]
省市区数据结构
没有想到一个简单的问题,最后竟然扯到了数据结构。经过尝试和思考,最终出现了三种数据结构,而这些东西应该都不是新鲜事。鉴于学识有限,我只能浅尝辄止的对比三者的异同,以及给出自己循环数据的方法。
1.无子父级关系的数组
去年做项目时省市区数据并没有从接口读取,而是保存到一个 JS 文件中。以下是后台从数据库导出的原始省市区数据片段(2016 年的数据,应该比较全,我删除了香港、澳门及台湾)。
[ { "region_id": 11, "region_name": "北京市", "region_sort": 1, "region_remark": "直辖市", "pinyin": "beijingshi", "py": "bjs", "area_code": "110000", "parent_id": 1, "level": 1 }, { "region_id": 12, "region_name": "天津市", "region_sort": 2, "region_remark": "直辖市", "pinyin": "tianjinshi", "py": "tjs", "area_code": "120000", "parent_id": 1, "level": 1 }, { "region_id": 13, "region_name": "河北省", "region_sort": 3, "region_remark": "省份", "pinyin": "hebeisheng", "py": "hbs", "area_code": "130000", "parent_id": 1, "level": 1 }, ... { "region_id": 101, "region_name": "北京市", "region_sort": 1, "region_remark": null, "pinyin": "beijingshi", "py": "bjs", "area_code": "110100", "parent_id": 11, "level": 2 }, { "region_id": 102, "region_name": "天津市", "region_sort": 2, "region_remark": null, "pinyin": "tianjinshi", "py": "tjs", "area_code": "120100", "parent_id": 12, "level": 2 }, { "region_id": 105, "region_name": "邯郸市", "region_sort": 5, "region_remark": null, "pinyin": "handanshi", "py": "hds", "area_code": "130400", "parent_id": 13, "level": 2 }, ... } ]
这个数据并没有明确的子父级关系,只能通过 parent_id 查找对应的省市。循环方式如下:
/** * [getProvince 获取省] * @param {[Object]} regions [省市区数据] * @return {[Array]} [省数组] */ function getProvince(regions) { $.each(regions, function() { if (this.level === 1) { province.push(this.region_name); } }); return province; } /** * [getCity 获取市] * @param {[Object]} regions [省市区数据] * @param {[String]} provinceName [省名] * @return {[Array]} [市数组] */ function getCity(regions, provinceName) { var province_id = 0, cityArr = []; $.each(regions, function() { if (this.level === 1 && this.region_name === provinceName) { province_id = this.region_id; return false; } }); $.each(regions, function() { if (this.level === 2 && this.parent_id === province_id) { cityArr.push(this.region_name) } }); return cityArr; } /** * [getArea 获取区] * @param {[Object]} regions [省市区数据] * @param {[String]} provinceName [省名] * @param {[String]} cityName [市名] * @return {[Array]} [区数组] */ function getArea(regions, provinceName, cityName) { var province_id = 0, city_id = 0, areaArr = []; $.each(regions, function() { if (this.level === 1 && this.region_name === provinceName) { province_id = this.region_id; } if (this.level === 2 && this.region_name === cityName && this.parent_id === province_id) { city_id = this.region_id; return false; } }); $.each(regions, function() { if (this.level === 3 && this.parent_id === city_id) { areaArr.push(this.region_name) } }); return areaArr; }
因为数据量不大,我使用了 jQuery 原生的 $.each 循环,而在平时的工作中,我更倾向于使用 JS 原生的 for 循环。
2.有子父级关系的数组
在之前做项目的时候,非常希望能够将第一种省市区结构转化成比较常用的具有子父级关系的结构数组。但那时不会用 Nodejs,也没有其它比较好的生成文件的方法,所以就一直使用第一种循环思路。
最终经过一阵折腾,成功用 Nodejs 实现了对原有数据结构的重新映射。
现在我使用 Nodejs 对省市区结构做了如下调整,因为本文的讨论重点是级联选择器以及数据结构,所以就不去讨论如何使用 Nodejs 生成文件了。
[ { "provinceName": "北京市", "provinceId": 11, "cities": [ { "cityName": "北京市", "cityId": 101, "areas": [ { "areaName": "东城区", "areaId": 1001 }, { "areaName": "西城区", "areaId": 1002 }, { "areaName": "崇文区", "areaId": 1003 }, { "areaName": "宣武区", "areaId": 1004 }, { "areaName": "朝阳区", "areaId": 1005 }, { "areaName": "丰台区", "areaId": 1006 }, { "areaName": "石景山区", "areaId": 1007 }, { "areaName": "海淀区", "areaId": 1008 }, { "areaName": "门头沟区", "areaId": 1009 }, { "areaName": "房山区", "areaId": 1010 }, { "areaName": "通州区", "areaId": 1011 }, { "areaName": "顺义区", "areaId": 1012 }, { "areaName": "昌平区", "areaId": 1013 }, { "areaName": "大兴区", "areaId": 1014 }, { "areaName": "怀柔区", "areaId": 1015 }, { "areaName": "平谷区", "areaId": 1016 }, { "areaName": "密云县", "areaId": 1017 }, { "areaName": "延庆县", "areaId": 1018 } ] } ] } ... ]
循环方式如下:
/** * [getProvince 获取省] * @param {[Object]} regions [省市区数据] * @return {[Array]} [省数组] */ function getProvince(regions) { var provinceArr = []; $.each(regions, function() { provinceArr.push(this.provinceName); }); return provinceArr; } /** * [getCity 获取市] * @param {[Object]} regions [省市区数据] * @param {[String]} provinceName [省名] * @return {[Array]} [市数组] */ function getCity(regions, provinceName) { var cityArr = []; $.each(regions, function(i, province) { if (province.provinceName === provinceName) { $.each(province.cities, function(j, city) { cityArr.push(city.cityName); }); return false; } }); return cityArr; } /** * [getArea 获取区] * @param {[Object]} regions [省市区数据] * @param {[String]} provinceName [省名] * @param {[String]} cityName [市名] * @return {[Array]} [区数组] */ function getArea(regions, provinceName, cityName) { var areaArr = []; $.each(regions, function(i, province) { if (province.provinceName === provinceName) { $.each(province.cities, function(j, city) { if (city.cityName === cityName) { $.each(city.areas, function(k, area) { areaArr.push(area.areaName); }); return false; } }); return false; } }); return areaArr; }
经过简单测试,这种数据结构确实优于第一种,但是两者循环时间的差距也仅在毫秒之间,所以实际感受并不深刻。
3.有子父级关系的对象
第二种数据结构是省市区数据常用的数据类型,但是想要获得选中省市所对应的 ID 不是很方便,需要重新循环一次。
最后尝试将省市区名称作为键值的对象类型。
{ "北京市": { "id": 11, "cities": { "北京市": { "id": 101, "areas": { "东城区": { "id": 1001 }, "西城区": { "id": 1002 }, "崇文区": { "id": 1003 }, "宣武区": { "id": 1004 }, "朝阳区": { "id": 1005 }, "丰台区": { "id": 1006 }, "石景山区": { "id": 1007 }, "海淀区": { "id": 1008 }, "门头沟区": { "id": 1009 }, "房山区": { "id": 1010 }, "通州区": { "id": 1011 }, "顺义区": { "id": 1012 }, "昌平区": { "id": 1013 }, "大兴区": { "id": 1014 }, "怀柔区": { "id": 1015 }, "平谷区": { "id": 1016 }, "密云县": { "id": 1017 }, "延庆县": { "id": 1018 } } } } } ... }
这样的变化使循环变得简单了,只用一层循环就可以:
/** * [getProvince 获取省] * @param {[Object]} regions [省市区数据] * @return {[Array]} [省数组] */ function getProvince(regions) { var provinceArr = []; $.each(regions, function(province) { provinceArr.push(province); }); return provinceArr; } /** * [getCity 获取市] * @param {[Object]} regions [省市区数据] * @param {[String]} provinceName [省名] * @return {[Array]} [市数组] */ function getCity(regions, provinceName) { var cityArr = []; $.each(regions[provinceName]['cities'], function(city) { cityArr.push(city); }); return cityArr; } /** * [getArea 获取区] * @param {[Object]} regions [省市区数据] * @param {[String]} provinceName [省名] * @param {[String]} cityName [市名] * @return {[Array]} [区数组] */ function getArea(regions, provinceName, cityName) { var areaArr = []; $.each(regions[provinceName]['cities'][cityName]['areas'], function(area) { areaArr.push(area); }); return areaArr; }
这种数据结构和第二种差不多,但是循环对象只能用 for in 形式,而 for in 是最不稳定的循环方式,所以这种数据结构会不会存在潜在的危险?虽然目前的数据量并不需要担心,但作为程序员,还是应该时刻把效率和性能放在第一位。
下图显示了三种文件的大小,都是未压缩的 JSON 格式。很显然,第三种数据结构最轻量,而第一种数据因为有多余的键值,所以尺寸非常庞大。
结论与思考
第二种数据结构和第三种数据结构差别不大,但是第三种数据结构可以更简单的获取省市 ID 。也许其中还有很多我所不知道的细枝末节,但我能力有限,无法深入展开讨论,只能从表面探索其中的异同。
整体而言,三种数据结构都有循环,所以第一级联动时或多或少会有性能的损耗。我突然在想有没有第四种数据结构,在对应的 key 值上有现成的数组,这样就不必再去循环了,答案也许是肯定的。
以下是省市区选择器的完整配置,联动效果需要使用上面提到的循环方法。所有的演示文件以及省市区 JSON 文件都上传到了 GitHub 。
// 初始化 Framework7 var myApp = new Framework7(); // 初始化省市区 var province = getProvince(regions), city = getCity(regions, '北京市'), area = getArea(regions, '北京市', '北京市'); // 保存 picker 选择的省 var provinceSelect = ''; // 省市区联动 / Framework7 picker var pickerLocation = myApp.picker({ input: '#location', rotateEffect: true, toolbarTemplate: '<div class="toolbar">\ <div class="toolbar-inner">\ <div class="left">\ <a href="#" class="link close-picker">取消</a>\ </div>\ <div class="right">\ <a href="#" class="link close-picker">完成</a>\ </div>\ </div>\ </div>', cols: [{ cssClass: 'f-s-14', width: '33.33%', textAlign: 'left', values: province, onChange: function(picker, province) { if (picker.cols[1].replaceValues) { provinceSelect = province; city = getCity(regions, province); area = getArea(regions, province, city[0]); picker.cols[1].replaceValues(city); picker.cols[2].replaceValues(area); } } }, { cssClass: 'f-s-14', width: '33.33%', textAlign: 'center', values: city, onChange: function(picker, city) { if (picker.cols[2].replaceValues) { area = getArea(regions, provinceSelect, city); picker.cols[2].replaceValues(area); } } }, { cssClass: 'f-s-14', width: '33.33%', textAlign: 'right', values: area, } ] });
很遗憾,CityPicker 并不是一个插件,只是对 Framework7 Picker 组件的具体应用。如果有需要的话,我也会考虑把它封装成一个插件。
感谢您的阅读,如果您对我的文章感兴趣,可以关注我的博客,我是叙帝利,下篇文章再见!
低代码平台必备轻量级 GUI 库 https://github.com/acrodata/gui
适用于 Angular 的 CodeMirror 6 组件 https://github.com/acrodata/code-editor
适用于 Angular 的水印组件(防删除,盲水印) https://github.com/acrodata/watermark
开发低代码平台的必备拖拽库 https://github.com/ng-dnd/ng-dnd
基于 Angular Material 的中后台管理框架 https://github.com/ng-matero/ng-matero
Angular Material Extensions 扩展组件库 https://github.com/ng-matero/extensions
Unslider 轮播图插件纯 JS 实现 https://github.com/nzbin/unsliderjs
仿 Windows 照片查看器插件 https://github.com/nzbin/photoviewer
仿 Windows 照片查看器插件 jQuery 版 https://github.com/nzbin/magnify
完美替代 jQuery 的模块化 DOM 库 https://github.com/nzbin/domq
简化类名的轻量级 CSS 框架 https://github.com/nzbin/snack
与任意 UI 框架搭配使用的通用辅助类 https://github.com/nzbin/snack-helper
单元素纯 CSS 加载动画 https://github.com/nzbin/three-dots
有趣的 jQuery 卡片抽奖插件 https://github.com/nzbin/CardShow
悬疑科幻电影推荐 https://github.com/nzbin/movie-gallery
锻炼记忆力的小程序 https://github.com/nzbin/memory-stake