最近在作移動端項目的時候遇到了省市區選擇的功能。以往作項目時都是省市區分開的下拉框樣式。此次但願實現效果圖要求的級聯選擇器。我是 Framework7 框架的忠實粉絲,慶幸的是 Framework7 已經有模擬 iOS 選擇框效果的 Picker 組件。在開發以前我先搜索了現有的一些選擇器插件,總體而言都能知足需求但都不完美,好比滑動不流暢、顯示有 Bug 等等。程序員
基於 Framework7 製做級聯選擇器比較簡單,關鍵是生成省市區數組以及省市區之間的聯動。github
如下是 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 = ['濱城區','惠民縣','陽信縣','博興縣',...]
沒有想到一個簡單的問題,最後居然扯到了數據結構。通過嘗試和思考,最終出現了三種數據結構,而這些東西應該都不是新鮮事。鑑於學識有限,我只能淺嘗輒止的對比三者的異同,以及給出本身循環數據的方法。數據結構
去年作項目時省市區數據並無從接口讀取,而是保存到一個 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 查找對應的省市。循環方式以下:ide
/** * [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 循環。性能
在以前作項目的時候,很是但願可以將第一種省市區結構轉化成比較經常使用的具備子父級關係的結構數組。但那時不會用 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; }
通過簡單測試,這種數據結構確實優於第一種,可是二者循環時間的差距也僅在毫秒之間,因此實際感覺並不深入。
第二種數據結構是省市區數據經常使用的數據類型,可是想要得到選中省市所對應的 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 組件的具體應用。若是有須要的話,我也會考慮把它封裝成一個插件。