树状菜单的前世今生
2017-05-28 22:42
281 查看
作者: code lighter
原创
日期: 2017/5/29
关键词: 树状菜单
、 分级菜单
、递归
、堆栈
、 字符串解析
、格式转换
很多时候,网站需要动态加载多级分类菜单,后台程序通过查询数据库生成分类菜单的嵌套数组,然后将嵌套数组转换成JSON格式输出给前端程序展示。类似下面这样:$data = [0=>["text"=>"生活电器", "id"=>1, "parent_id"=>0, "children"=>[0=>["text"=>"空调", "id"=>2, "parent_id"=>1], 1=>["text"=>"冰箱", "id"=>3, "parent_id"=>1] ] ], 1=>["text"=>"男装", "id"=>4, "parent_id"=>0 ];
我们发现上面这种嵌套数据的添加会非常麻烦,需要一个一个手动添加,很难一次性批量导入数据库。因为子类菜单的parent_id需要指向父类的id,如果在平时测试的时候,需要录入这种层级菜单数据,将会非常头疼。那么有没有一种快速批量录入这种分类菜单的方法呢?当然有,比如我灵机一动,想到了下面这种方法.
打开txt文件,设置编码格式为utf-8,录入下面这种格式的数据,一行代表一个菜单,一个 + 号表示一级子菜单,二个 + 号表示二级子菜单,依此类推。顶级菜单不需要 + 号,录入的时候记得前后不要留空格,这样看上去更直观。仔细观察这种数据结构,你会发现它暗含了菜单的层级关系,而且录入十分方便。
生活电器
+空调
++智能空调++变频空调
+冰箱
++三门冰箱++对开门冰箱
男装
+清凉夏装
+温情冬装
程序读入上面的txt数据,然后自动生成下面这种JSON嵌套数据结构,非常方便程序进一步处理。比如可以批量导入数据库,作为缓存文件直接返回给网站前端程序等.[ { "id": 1, "text": "生活电器", "level": 0, "parent_id": 0, "children": [ { "id": 2, "text": "空调", "level": 1, "parent_id": 1, "children": [ { "id": 3, "text": "智能空调", "level": 2, "parent_id": 2 }, { "id": 4, "text": "变频空调", "level": 2, "parent_id": 2 } ] }, { "id": 5, "text": "冰箱", "level": 1, "parent_id": 1, "children": [ { "id": 6, "text": "三门冰箱", "level": 2, "parent_id": 5 }, { "id": 7, "text": "对开门冰箱", "level": 2, "parent_id": 5 } ] } ] }, { "id": 8, "text": "男装", "level": 0, "parent_id": 0, "children": [ { "id": 9, "text": "清凉夏装", "level": 1, "parent_id": 8 }, { "id": 10, "text": "温情冬装", "level": 1, "parent_id": 8 } ] } ]
那么问题来了,程序如何读取txt文件里的分级菜单数据,并识别每级菜单的层级关系呢?如果我们一边读取一边解析,会发现程序需要不断回溯查看,处理起来会非常麻烦。比如当处理到 冰箱 这一行的时候,程序需要回溯查看父级菜单 生活电器 并将 冰箱 的
parent_id指向 生活电器 的
id,而处理到 男装 这一行的时候
parent_id = 0,程序的状态迁移并不好控制。
在处理类似这种比较棘手的编程问题的时候, 我们要转变思维。借鉴数学领域里经常用到的化归思想,不断将问题进行转化,转化到到我们熟悉的问题上来。
在PHP项目中,生成分类层级菜单,我们一般采用的方法是从数据库中读取出分类菜单数据[一个二维数组],然后根据每行数据的
id, parent_id递归调用菜单生成函数,将二维数组转化为嵌套数组,如下所示
public function buildTree(&$array,$callback=null,$parent_id=0,$child_node="children"){ $tree = []; foreach($array as $k=>$v){ if($v['parent_id'] == $parent_id){ unset($array[$k]); $tmp =is_callable($callback)?call_user_func($callback,$v):$v; $children = $this->buildTree($array,$callback,$v['id'],$child_node); if($children){ $tmp[$child_node] = $children; } $tree[] = $tmp; } } return $tree; }
上面这个函数的
callback参数是一个回调函数,是用来过滤每行数据的。回调函数里,是简单的赋值语句,取出想要的字段,比如
menu_name, is_visible, id, parent_id。当然
id,parent_id是必须取出来的,因为他们标识了菜单的层级关系。注意程序里面的
unset($array[$k]);这一句代码主要是为了提高递归程序的性能,每次递归的时候,数组会变小。这样一来,已经遍历过的数据,就不会重复遍历了。
有了上面的函数做铺垫,我们发现只要找到每行数据的id,parent_id的关系,就能调用上面这个菜单函数生成想要的多维嵌套数组。
怎么确定这种关系是关键,也是难点。我们分步来做
步骤 1. 遍历每行菜单数据,根据每行数据前面的+号,标识每行的层级level
步骤 2. 在 步骤 1 基础上我们再次遍历每行菜单数据, 根据每行数据的
level大小,我们来设置每个菜单的
parent_id从而确定菜单之间的关系,遍历的过程我们发现需要查看之前菜单的
id, 而程序不能确定当前行的菜单的父类到底是遍历过的菜单行的哪一个。这个时候我们需要采用堆栈数据结构,将每次遍历菜单的
id压栈, 当层级深的菜单遍历到层级浅的菜单,我们再将数据出栈。比如遍历到 冰箱 这一行的时候,我们需要弹出到 生活电器 这一层。具体代码如下.
public function markTree(&$data){ $level = $parent_id = 0; foreach($data as $k=>$v){ if($v['level']>$level){ $this->_stack->push(["level"=>$v['level'],"id"=>$v['id']]); $data[$k]['parent_id'] =$data[$k-1]['id']; $level = $v['level']; $parent_id = $data[$k-1]['id']; }else if($v['level']<$level){ $parent_id = $this->findParent($v['level']-1); $data[$k]['parent_id'] = $parent_id; $level = $v['level']; }else { $this->_stack->push(["level"=>$v['level'],"id"=>$v['id']]); $data[$k]['parent_id'] = $parent_id; } } }
总结, 在处理复杂问题的时候,我们发现分步解决是一个很好的手段,也是常见的工程思想。比如火箭制造,程序编译,TCP/IP协议 莫不如是。甚至CPU芯片这么高级的东西,都借鉴了流水线生产的思想,设计了多级指令缓存,提高程序执行效率。
完整程序源码:
<?php /** * Author: code lighter * Date: 2017/5/28 * Time: 21:14 * Function: translate the plain menu data to a nested tree array in PHP * The input plain menu data format as follows: * top_menu * +sub_menu * ++sub_menu * top_menu * The program will translate above data to php nested array * $output = [ * 0=>["text"=>"top_menu", * "id"=>1, * "parent_id=>0, * "children"=>[ * "text"=>"sub_menu", * "id"=>2, * "parent_id"=>1, * "children"=>[ * "text"=>"sub_menu", * "id"=>3, * "parent_id"=>2 * ] * ] * ], * 1=>["text="top_menu", * "id"=>4, * "parent_id"=>0 * ] * ]; */ namespace company\code_lighter; /* * The stack class to stock the depth of the menu */ class Stack { public $_stack_size; public $_ptr; public $_container; public function __construct() { $this->_container = []; $this->_stack_size = 20; $this->_ptr = -1; } public function isEmpty() { return $this->_ptr==-1; } public function isFull() { return $this->_ptr == $this->_stack_size-1; } public function push($data) { if($this->isFull()) return false; $this->_container[] = $data; $this->_ptr++; } public function pop(){ if($this->isEmpty()) return false; $this->_ptr--; return array_pop($this->_container); } } class MenuTree { private $_filePath; //file path private $_loaded; //mark file loaded or not private $_errors; private $_data; // the input menu data private $_parsed_data; // each line include a level property to mark the menu depth. private $_cur_pos; // the current parsed char private $_current_line_words; // the length of each line private $_stack; // stack for parse hierarchy retionship between parent menus and its sub menus public function __construct($filePath) { $this->_filePath = $filePath; $this->_loaded= false; $this->_errors = []; $this->_data = []; $this->_parsed_data = []; $this->_cur_pos = 0; $this->_current_line_words = 0; $this->_stack= new Stack; } public function log($error_code,$error_msg){ $_errors[] = ['error_code'=>$error_code,'error_msg'=>$error_msg]; } public function hasError(){ return count($this->_errors); } public function readFile() { if($this->_loaded) return; $file = fopen(dirname(__FILE__).'/'.$this->_filePath,'r'); if(!$file){ $this->log(601,"can't open file ".$this->_filePath); } while(!feof($file)){ $this->_data[] = fgets($file); } fclose($file); $this->_loaded = true; } public function getData(){ if(!$this->hasError()){ if(!$this->_loaded){ $this->readFile(); } return $this->_data; } return false; } public function getNextChar(&$line) { if($this->_cur_pos<$this->_current_line_words){ $char = mb_substr($line,$this->_cur_pos,1,'utf-8'); $this->_cur_pos ++; return $char; } $this->_cur_pos++; return false; } // parse the + sign before each line public function parse(){ $this->readFile(); if(!$this->hasError()){ $data = []; foreach($this->_data as $k=>$line){ $this->_current_line_words = mb_strlen($line,'utf-8'); $level =0; while($this->getNextChar($line)=='+'){ $level++; } do{ $char = $this->getNextChar($line); }while($char != "\r" && $char != false); $_line = [ 'id'=>$k+1, 'text'=>mb_substr($line,$level,$this->_cur_pos-1 -$level,'utf-8'), 'level'=>$level ]; $data[] = $_line; $this->_cur_pos = 0; } return $data; } return []; } // mark the hierarchy relationship of each line public function markTree(&$data){ $level = $parent_id = 0; foreach($data as $k=>$v){ if($v['level']>$level){ $this->_stack->push(["level"=>$v['level'],"id"=>$v['id']]); $data[$k]['parent_id'] =$data[$k-1]['id']; $level = $v['level']; $parent_id = $data[$k-1]['id']; }else if($v['level']<$level){ $parent_id = $this->findParent($v['level']-1); $data[$k]['parent_id'] = $parent_id; $level = $v['level']; }else { $this->_stack->push(["level"=>$v['level'],"id"=>$v['id']]); $data[$k]['parent_id'] = $parent_id; } } } // look for the parent id of the current menu in the stack public function findParent($level){ while(!$this->_stack->isEmpty()){ $data = $this->_stack->pop(); if($data['level'] == $level){ return $data['id']; } } return 0; } // convert two dimension array into nested array public function buildTree(&$array,$callback=null,$parent_id=0,$child_node="children"){ $tree = []; foreach($array as $k=>$v){ if($v['parent_id'] == $parent_id){ unset($array[$k]); $tmp =is_callable($callback)?call_user_func($callback,$v):$v; $children = $this->buildTree($array,$callback,$v['id'],$child_node); if($children){ $tmp[$child_node] = $children; } $tree[] = $tmp; } } return $tree; } // output the nested array to json object public function toJson() { $data = $this->parse(); $this->markTree($data); $tree = $this->buildTree($data); return $tree; } }
测试例程
namespace company\controllers; use Yii; use yii\web\Controller; use yii\web\Response; use company\code_lighter\MenuTree; class TestController extends Controller { public $enableCsrfValidation = false; public function actionMenuTree() { Yii::$app->response->format = Response::FORMAT_JSON; // following lines are the test code $menuTree = new MenuTree("menu.txt"); return $menuTree->toJson(); } }
相关文章推荐
- (转)基于Web标准的UI组件 — 树状菜单(1)
- 基于Web标准的UI组件 ― 树状菜单(2)
- F7显示物料时带树状菜单
- 自写JQ控件-树状菜单控件[demo下载]
- 基于asp.net ajax 的异步加载无限级联树状菜单
- VS2005利用TreeView控件和递归函数从数据库动态生成树状菜单
- VS2005利用TreeView控件和递归函数动态生成树状菜单
- 仿通达OA三层树状的菜单
- Ext2.0含有checkbox的树状菜单
- F7显示时带树状菜单
- 呈现树状结构的菜单
- php从数据库读取菜单数据并树状显示
- C#winform菜单权限分配,与菜单同步的treeView树状菜单权限控制使用心得
- 框架与树状结构菜单的结合
- 无限级树状菜单(转)
- C# 递归显示 TreeView 树状菜单树!
- 左侧树状菜单代码
- Struts2树状菜单三种实现方式
- 树状导航菜单