由移动端级联选择器所引发的对于数据结构的思考

GitHub:https://github.com/nzbin/Framework7-CityPicker

Demo:https://nzbin.github.io/Framework7-CityPicker/

前言

最近在做移动端项目的时候遇到了省市区选择的功能。以往做项目时都是省市区分开的下拉框样式。这次希望实现效果图要求的级联选择器。我是 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 组件的具体应用。如果有需要的话,我也会考虑把它封装成一个插件。

时间: 2024-10-29 19:07:12

由移动端级联选择器所引发的对于数据结构的思考的相关文章

移动端时间选择器(更新1.2.0版本)

开发的一个简单的移动端时间选择器,支持多种主题,现在来共享一下. Github地址:https://github.com/IronPans/LazyPicker 效果图: 你也可以扫描二维码用手机体验: 1.2版本体验: 如果你使用PC浏览器,那么你需要切换到手机模式才可使用. 步骤:按F12或鼠标右键--点击检查,再点击控制台左上角的手机模式按钮即可. 地址:点这里 使用方法 引入CSS和JavaScript脚本: <link rel="stylesheet" href=&qu

element-ui Cascader 级联选择器示例

<html> <head>test</head> <style> @import url("http://unpkg.com/[email protected]/lib/theme-chalk/index.css"); </style> <body> <script src="http://unpkg.com/vue/dist/vue.js"></script> <

vue+element ui项目总结点(一)select、Cascader级联选择器、encodeURI、decodeURI转码解码、一级mockjs用法、路由懒加载三种方式

不多说上代码: <template> <div class="hello"> <h1>{{ msg }}</h1> <p>Element UI简单Cascader级联选择器使用</p> <el-cascader :options='options' v-model="selectedOptions" @change="handleChange"> </el-

Vue省市区三级联选择器V-Distpicker的使用

Vue省市区三级联选择器V-Distpicker的使用 最近用的Vue+Element UI时,有些地方需要用到省市区三联选择器,网上安装并尝试了多种类似的插件,但都因为无法正常实现或是没有眼缘而弃用了.最后选择的是V-Distpicker这种,配置和使用起来还算是比较简单的,下面就分享一下其用法和需要注意的点(需要踩过的坑). 1.使用方法 安装和文档,请查看官网https://distpicker.pigjian.com/ ? 或者 同性交友网站https://github.com/jcc/

基于element ui的级联选择器组件实现的分类后台接口

今天在做资产管理系统的时候遇到一个分类的级联选择器,前端是用的element的组件,需要后台提供接口支持.     这个组件需要传入的数据结构大概是这样的,详细的可参考官方案例: [{ value: '1001', label: 'IT固定资产', children: [{ value: '100101', label: '服务器' }, { value: '100102', label: '笔记本' }, { value: '100103', label: '平板电脑' }, { value:

vue+element级联选择器对接后台数据

1.后台接口返回的数据肯定要和级联选择器的数据一致,所以我专门弄个model存放返回的值,如下:/** * @Auther: GGDong * @Date: 2019/4/3 10:30 */@Getter@Setterpublic class ServerList{ //值 private String value; //名称 private String label; //子级 private List<ServerList> children;} 2.最主要的就是写sql了,不过幸好有M

ElementUI 之 Cascader 级联选择器指定 value label

ElementUI 的 Cascader 级联选择器个人觉得很好用,但是对 :options="options" 里的数据格式是有特定要求的:input 框显示的值是 options 里的 label 值.如果 options 的键值对不是 value label ,就需要 props 来配置. 如何配置 value label? <el-cascader v-model="data" :options="options" placehol

element-ui el-cascader级联选择器设置指定层级不能选中

有时候用element-ui el-cascader级联选择器添加分类时会遇到最多添加几级的限定.看了文档,只要给需要禁止选择的选项添加disabled属性就可以.但是使用一层一层循环遍历数据感觉很麻烦,自己写了个遍历的方法,纪录下,方便以后使用 贴代码 cascader.vue <template> <el-dialog title="添加分类" :visible.sync="dialogVisible" width="500px&qu

关于级联选择器el-cascader的一些属性

级联选择器如下: 基本用法: <el-cascader v-model="value" :options="options" @change="handleChange"> </el-cascader> 特性: 1.后端返回的数据格式名称跟前端需要的名称不一致: 使用:props动态改变 :props="optionProps" optionProps: { value: 'id', label: 'n