찾다
백엔드 개발PHP 튜토리얼无限极分类原理与实现

前言

无限极分类是我很久前学到知识,今天在做一个项目时,发现对其概念有点模糊,所以今天就来说说无限极分类。

首先来说说什么是无限极分类。按照我的理解,就是对数据完成多次分类,如同一棵树一样,从根开始,到主干、枝干、叶子……

完成无限极分类,主要运用了两种方法,一是递归方式,二是迭代方式。而主要运用无限极分类的地方有地址解析,面包屑导航等等。下面就来具体介绍两种方法的原理及实现方法。

家谱树与子孙树

家谱树是无限极分类的表现形式之一,另一个是子孙树。一开始学习无限极分类时,我时常弄混这两棵树,现在看来自然是明白很多。从汉语的意思也能够看出其中的区别。

家谱,现在很多地方都流行起修家谱,那怎么修家谱,按照我理解,就是给自己找一个祖宗,一代代找上去,形成了一个体系,这样编篡而成的叫家谱。家谱树就与之类似,从某个节点开始向上寻找其父节点,再找父节点的父节点,直到找不到为止。按照这种寻找,形成的一个类似树状的结构,就叫做家谱树。

而子孙树与其相反,子孙树类似于生物书中的遗传图,从某个节点开始寻找它的子节点,再找子节点的子节点,直到寻找完毕。这样形成的树状结构就叫做子孙树。

从上面对家谱树与子孙树的描述,将其转换为代码时,我的第一印象就是利用递归方式,家谱树,找父节点的父节点,子孙树,找子节点的子节点。完全符合递归思想。所以首先我们来说说利用递归方式完成家谱树与子孙树。

递归方式

家谱树的实现

为更清楚的讲解,我先将即将分类的数据贴在下面,是关于地址的数据:

$address = array(    array('id'=>1  , 'address'=>'安徽' , 'parent_id' => 0),    array('id'=>2  , 'address'=>'江苏' , 'parent_id' => 0),    array('id'=>3  , 'address'=>'合肥' , 'parent_id' => 1),    array('id'=>4  , 'address'=>'庐阳区' , 'parent_id' => 3),    array('id'=>5  , 'address'=>'大杨镇' , 'parent_id' => 4),    array('id'=>6  , 'address'=>'南京' , 'parent_id' => 2),    array('id'=>7  , 'address'=>'玄武区' , 'parent_id' => 6),    array('id'=>8  , 'address'=>'梅园新村街道', 'parent_id' => 7),    array('id'=>9  , 'address'=>'上海' , 'parent_id' => 0),    array('id'=>10 , 'address'=>'黄浦区' , 'parent_id' => 9),    array('id'=>11 , 'address'=>'外滩' , 'parent_id' => 10)    array('id'=>12 , 'address'=>'安庆' , 'parent_id' => 1)    );

按照上文的介绍,对上面数据进行家谱树无限极分类,假设我们想要寻找大杨镇的家谱树,先找到与之相关的信息。

'id'=>5  , 'address'=>'大杨镇' , 'parent_id' => 4

可以看出它的父节点的id,即parent_id == 4,那么id==4的节点就是其父节点,由此找到庐阳区:

'id'=>4  , 'address'=>'庐阳区' , 'parent_id' => 3

与上面类似,寻找id=3的节点,依次向上寻找,找到大杨镇的家谱

大杨镇 -> 庐阳区 -> 合肥 -> 安徽

那么怎么用代码来完成它呢?其实很简单,只需要判断寻找的父id是否与节点的id相等,即parent_id ?= id,相等就是要寻找的父节点,并把该节点的parent_id作为寻找的id,递归进行寻找。如下面的流程图:

递归方法求家谱树

下面就开始编写代码:

/** * 获取家谱树 * @param   array        $data   待分类的数据 * @param   int/string   $pid    要找的祖先节点 */function Ancestry($data , $pid) {    static $ancestry = array();    foreach($data as $key => $value) {        if($value['id'] == $pid) {            $ancestry[] = $value;            Ancestry($data , $value['parent_id']);        }    }    return $ancestry;}

根据流程图,代码编写完成。注意上面存储结点的数组,即$ancestry,要添加静态化关键字static,否则每次递归都会将该数组初始化。当然也可以使用array_merge将每次返回的数组与上一次的进行合并。

寻找家谱的关键就是条件判断,寻找的parent_id等于某个节点的id值,显然该节点就是要寻找的父节点。

代码编写完成,来看看是否符合我们的预期,来寻找大杨镇的家谱:

Ancestry($address , 4);

结果:

Array(    [0] => Array        (            [id] => 4            [address] => 庐阳区            [parent_id] => 3        )    [1] => Array        (            [id] => 3            [address] => 合肥            [parent_id] => 1        )    [2] => Array        (            [id] => 1            [address] => 安徽            [parent_id] => 0        ))

可以看出结果与我们预期相符。那么家谱树的递归方法就完成了,下面来讲子孙树的实现。

子孙树的实现

依然使用上面的数据,子孙树是从父节点开始,向下寻找其子孙节点,而形成的一个树状图形。

假设寻找id=0的子孙节点,那么就要注意所有parent_id=0的节点,这些节点都是id=0的子节点。然后,把parent_id=0节点的id作为查询id继续向下查询,直到查不到任何子节点为止。如下:

子孙树

流程图如下:

子孙树流程图

其流程与家谱树类似,不同点,也是关键点就是条件语句的执行。家谱树判断的是当前节点的id是否与上一个节点的parent_id相等;子孙树判断的是当前节点的parent_id与上一个节点的id相等,按照这种条件判断子孙树能够有多个子孙节点,而家谱树只能存在一个祖先。代码如下:

/** * 获取子孙树 * @param   array        $data   待分类的数据 * @param   int/string   $id     要找的子节点id * @param   int          $lev    节点等级 */ function getSubTree($data , $id = 0 , $lev = 0) {     static $son = array();     foreach($data as $key => $value) {         if($value['parent_id'] == $id) {             $value['lev'] = $lev;             $son[] = $value;             getSubTree($data , $value['id'] , $lev+1);         }     }     return $son; }

在函数中我添加了一个变量lev,为的是给存入的节点标注等级,方便看出子孙树的结构。下面来测试结果:

getSubTree($data , 0 , 0);

因篇幅有限,将结果进行部分处理:

foreach($tree as $k => $v) {    echo str_repeat('--' , $v['lev']) . $v['address'] . '<br/>';}

结果:

安徽--合肥----庐阳区------大杨镇--安庆江苏--南京----玄武区------梅园新村街道上海--黄浦区----外滩

递归方式的家谱树与子孙树比较容易理解,只要对递归思想比较了解,一步步写下来不是很难。比起递归方式,迭代方式可能更加让人难以理解。下面就来介绍迭代方式的家谱树与子孙树编写。

迭代方式

家谱树

完成跌代方式的家谱树之前,首先说一下寻找祖先节点的终止条件。虽然叫无限极分类,它不是绝对的无限,只是理论的无限。

如同我国上下五千年历史,任一个大的姓氏,向上找其祖先,不是找到炎帝就是找到黄帝,在往前就没有历史记载了。所以在家谱树的寻找中也有终止条件,就是在分类数据中再也找不到它的父节点时,表现在实例数据上,就是不存在parent_id

这也是完成迭代的关键,以其作为迭代条件,对数据进行循环判断,并把每次找到的节点的parent_id再次作为迭代条件,直到不满足迭代条件。流程图如下:

家谱树迭代流程

理清流程,现在开始完成代码编写:

function Ancestry($data , $pid) {    $ancestry = array();    while($pid > 0) {        foreach($data as $v) {            if($v['id'] == $pid) {                $ancestry[] = $v;                $pid = $v['parent_id'];            }        }    }    return $ancestry;}

迭代条件$pid>0,当pid>0时说明还有祖先存在,可以继续迭代,否则说明没有祖先,迭代终止。$pid = $v['parent_id']是迭代继续进行的关键,每次找到祖先节点,就将祖先节点的父id传递给pid,进行下一次迭代。

运行这个函数,结果与使用递归方式的结果一致。

子孙树的实现

使用迭代方式完成子孙树,更为复杂,需要运用的栈的思想。在进行迭代的过程中,将每次寻找的id入栈,找到一个节点,就将该节点从原数据中删除,当寻找到叶子节点时,即不存在子孙节点时,就将该叶子节点对应的id从栈中弹出,再寻找栈顶id的子孙节点,直到栈清空为止,迭代结束。下面用一个例子来说明:

$address = array(    array('id'=>1  , 'address'=>'安徽' , 'parent_id' => 0),    array('id'=>2  , 'address'=>'江苏' , 'parent_id' => 0),    array('id'=>3  , 'address'=>'合肥' , 'parent_id' => 1),    array('id'=>4  , 'address'=>'庐阳区' , 'parent_id' => 3),    array('id'=>5  , 'address'=>'大杨镇' , 'parent_id' => 4),    array('id'=>6  , 'address'=>'南京' , 'parent_id' => 2),    array('id'=>7  , 'address'=>'玄武区' , 'parent_id' => 6),    array('id'=>8  , 'address'=>'梅园新村街道', 'parent_id' => 7),    array('id'=>9  , 'address'=>'上海' , 'parent_id' => 0),    array('id'=>10 , 'address'=>'黄浦区' , 'parent_id' => 9),    array('id'=>11 , 'address'=>'外滩' , 'parent_id' => 10)    array('id'=>12 , 'address'=>'安庆' , 'parent_id' => 1)    );

寻找id=0的子孙节点,id=0入栈,寻找到该节点,为

array('id'=>1  , 'address'=>'安徽' , 'parent_id' => 0)

此时栈为[0],并且将该节点从原数据中删除,再将id=1入栈,寻找id=1的子孙节点,找到为:

array('id'=>3  , 'address'=>'合肥' , 'parent_id' => 1),

此时栈[0][1],将该节点删除,id=3入栈,寻找id=3的子孙节点,找到:

array('id'=>4  , 'address'=>'庐阳区' , 'parent_id' => 3)

栈[0][1][3],将该节点删除,id=4入栈,寻找id=4的子孙节点,找到:

array('id'=>5  , 'address'=>'大杨镇'         , 'parent_id' => 4),

栈[0][1][3][4],将该节点删除,id=5入栈,栈[0][1][3][4][5],并寻找id=5的子节点,遍历后未找到,于是将id=5出栈,再次寻找id=4的子孙节点,依次进行。最后完成整个迭代。

期间,栈的情况如下:

[0][0][1][0][1][3][0][1][3][4][0][1][3][4][5][0][1][3][4][0][1][3][0][1][0][1][12][0][1][0]……

代码如下:

function getSubTree($data , $id = 0) {    $task = array($id);                          # 栈 任务表    $son = array();    while(!empty($task)) {        $flag = false;                           # 是否找到节点标志        foreach($data as $k => $v) {            # 判断是否是子孙节点的条件 与 递归方式一致            if($v['parent_id'] == $id) {                $son[] = $v;                     # 节点存入数组                array_push($task , $v['id']);    # 节点id入栈                $id = $v['id'];                  # 判断条件切换                unset($data[$k]);                # 删除节点                $flag = true;                    # 找到节点标志            }        }        # flag == false说明已经到了叶子节点 无子孙节点了        if($flag == false) {            array_pop($task);                    # 出栈            $id = end($task);                    # 寻找栈顶id的子节点        }    }    return $son;}

这里找到节点后必须把该节点从原数据中删除,否则会造成每次都找到该节点,形成无限迭代的bug。在这里利用数组函数array_push与array_pop模拟进栈与出栈操作。

利用迭代完成子孙树比较复杂,且我没有测试过这个与递归方式谁的效率高,不过利用迭代完成家谱树明显比起递归方法效率高。

应用

面包屑导航

说完了无限极分类的实现原理与方法,现在来说说在网站中对无限极分类的应用。最常用的就是面包屑导航了。

什么是面包屑导航,这个称呼来自于童话故事"汉赛尔和格莱特",具体什么故事就不叙述了,有兴趣的可以去谷歌一下。面包屑导航的作用就是告诉访问者他们目前在网站中的位置以及如何返回。下图就是一个典型的面包屑导航。

面包屑导航

面包屑是一个典型家谱树的应用,不要看它是从左到右,分类级数越来越低,就认为它是子孙树应用,要知道子孙树是可能存在多个分支,而面包屑导航要求的是一条主干。

将上面家谱树代码做一定修改,就能够完成面包屑导航。我们采用递归方式的家谱树。代码如下:

function Ancestry($data , $pid) {    static $ancestry = array();    foreach($data as $key => $value) {        if($value['id'] == $pid) {            Ancestry($data , $value['parent_id']);            $ancestry[] = $value;                        }    }    return $ancestry;}

如果先进行递归调用,在递归结束再将找到的节点存入数组中,就能够使祖先节点排列在数组前列,子孙节点排列在数组后列,方便进行提取数据。

简化演示步骤,不从数据库中取出数据,改为模拟数据:

 $tmp = array(    array('cate_id'=1 , 'name'=>'首页' , 'parent_id'=>'0'),    array('cate_id'=2 , 'name'=>'新闻中心' , 'parent_id'=>'1'),    array('cate_id'=3 , 'name'=>'娱乐新闻' , 'parent_id'=>'2'),    array('cate_id'=4 , 'name'=>'军事要闻' , 'parent_id'=>'2'),    array('cate_id'=5 , 'name'=>'体育新闻' , 'parent_id'=>'2'),    array('cate_id'=6 , 'name'=>'博客' , 'parent_id'=>'1'),    array('cate_id'=7 , 'name'=>'旅游日志' , 'parent_id'=>'6'),    array('cate_id'=8 , 'name'=>'心情' , 'parent_id'=>'6'),    array('cate_id'=9 , 'name'=>'小小说' , 'parent_id'=>'6'),    array('cate_id'=9 , 'name'=>'明星' , 'parent_id'=>'3'),    array('cate_id'=9 , 'name'=>'网红' , 'parent_id'=>'3')    );

假设用户点进明星导航,那么在网站显示的导航为:

$tree = Ancestry($tmp , 10);foreach ($tree as $key => $value) {    echo $value['name'] . '>';}

面包屑导航

防止设置父类为子类

在网站建立中,可能会碰到用户进行编辑时出现误操作,将某个栏目的父节点设置成了该栏目的子节点,进行这样的设置后会导致数据库中的数据丢失,因此在进行数据更新之前应该注意这一点。

利用家谱树,就能够避免发生这种错误。在用户提交表单时,我们将即将修改栏目的父节点的家谱树取出,并对家谱树进行遍历,如果发现该家谱树中发现了要修改的节点,就说明是错误操作。有点绕,举个例子来说明:

修改栏目新闻中心的父节点为娱乐新闻,就把娱乐新闻的家谱树取出来:

娱乐新闻 新闻中心 首页

在该家谱树中发现要修改的节点,新闻中心,那么说明出现了错误。具体代码如下:

$data = Ancestry($tmp , 3);foreach ($data as $key => $value) {    if($value['cate_id'] == 3) {        echo  'Error';        break;    }}

结语

对无限极分类的讲解就写到这儿,希望能够给对无限极分类存在迷惑的同学一定的灵感。在下才疏学浅,可能在描述中存在错漏,希望看到的同学能够指出。

同时在此,感谢布尔教育的刘道成,即燕十八老师,从他的教学视频中学到很多,这次重新看无限极分类,燕老师的视频给了我很多帮助。再次感谢燕老师。

성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
스칼라 유형, 반환 유형, 노조 유형 및 무효 유형을 포함한 PHP 유형의 힌트 작업은 어떻게 작동합니까?스칼라 유형, 반환 유형, 노조 유형 및 무효 유형을 포함한 PHP 유형의 힌트 작업은 어떻게 작동합니까?Apr 17, 2025 am 12:25 AM

PHP 유형은 코드 품질과 가독성을 향상시키기위한 프롬프트입니다. 1) 스칼라 유형 팁 : PHP7.0이므로 int, float 등과 같은 기능 매개 변수에 기본 데이터 유형을 지정할 수 있습니다. 2) 반환 유형 프롬프트 : 기능 반환 값 유형의 일관성을 확인하십시오. 3) Union 유형 프롬프트 : PHP8.0이므로 기능 매개 변수 또는 반환 값에 여러 유형을 지정할 수 있습니다. 4) Nullable 유형 프롬프트 : NULL 값을 포함하고 널 값을 반환 할 수있는 기능을 포함 할 수 있습니다.

PHP는 객체 클로닝 (클론 키워드) 및 __clone 마법 방법을 어떻게 처리합니까?PHP는 객체 클로닝 (클론 키워드) 및 __clone 마법 방법을 어떻게 처리합니까?Apr 17, 2025 am 12:24 AM

PHP에서는 클론 키워드를 사용하여 객체 사본을 만들고 \ _ \ _ Clone Magic 메소드를 통해 클로닝 동작을 사용자 정의하십시오. 1. 복제 키워드를 사용하여 얕은 사본을 만들어 객체의 속성을 복제하지만 객체의 속성은 아닙니다. 2. \ _ \ _ 클론 방법은 얕은 복사 문제를 피하기 위해 중첩 된 물체를 깊이 복사 할 수 있습니다. 3. 복제의 순환 참조 및 성능 문제를 피하고 클로닝 작업을 최적화하여 효율성을 향상시키기 위해주의를 기울이십시오.

PHP vs. Python : 사용 사례 및 응용 프로그램PHP vs. Python : 사용 사례 및 응용 프로그램Apr 17, 2025 am 12:23 AM

PHP는 웹 개발 및 컨텐츠 관리 시스템에 적합하며 Python은 데이터 과학, 기계 학습 및 자동화 스크립트에 적합합니다. 1.PHP는 빠르고 확장 가능한 웹 사이트 및 응용 프로그램을 구축하는 데 잘 작동하며 WordPress와 같은 CMS에서 일반적으로 사용됩니다. 2. Python은 Numpy 및 Tensorflow와 같은 풍부한 라이브러리를 통해 데이터 과학 및 기계 학습 분야에서 뛰어난 공연을했습니다.

다른 HTTP 캐싱 헤더 (예 : 캐시 제어, ETAG, 최종 수정)를 설명하십시오.다른 HTTP 캐싱 헤더 (예 : 캐시 제어, ETAG, 최종 수정)를 설명하십시오.Apr 17, 2025 am 12:22 AM

HTTP 캐시 헤더의 주요 플레이어에는 캐시 제어, ETAG 및 최종 수정이 포함됩니다. 1. 캐시 제어는 캐싱 정책을 제어하는 ​​데 사용됩니다. 예 : 캐시 제어 : Max-AGE = 3600, 공개. 2. ETAG는 고유 식별자를 통해 리소스 변경을 확인합니다. 예 : ETAG : "686897696A7C876B7E". 3. Last-modified는 리소스의 마지막 수정 시간을 나타냅니다. 예 : 마지막으로 변형 : Wed, 21oct201507 : 28 : 00GMT.

PHP에서 보안 비밀번호 해싱을 설명하십시오 (예 : Password_hash, Password_Verify). 왜 MD5 또는 SHA1을 사용하지 않습니까?PHP에서 보안 비밀번호 해싱을 설명하십시오 (예 : Password_hash, Password_Verify). 왜 MD5 또는 SHA1을 사용하지 않습니까?Apr 17, 2025 am 12:06 AM

PHP에서 Password_hash 및 Password_Verify 기능을 사용하여 보안 비밀번호 해싱을 구현해야하며 MD5 또는 SHA1을 사용해서는 안됩니다. 1) Password_hash는 보안을 향상시키기 위해 소금 값이 포함 된 해시를 생성합니다. 2) Password_verify 암호를 확인하고 해시 값을 비교하여 보안을 보장합니다. 3) MD5 및 SHA1은 취약하고 소금 값이 부족하며 현대 암호 보안에는 적합하지 않습니다.

PHP : 서버 측 스크립팅 언어 소개PHP : 서버 측 스크립팅 언어 소개Apr 16, 2025 am 12:18 AM

PHP는 동적 웹 개발 및 서버 측 응용 프로그램에 사용되는 서버 측 스크립팅 언어입니다. 1.PHP는 편집이 필요하지 않으며 빠른 발전에 적합한 해석 된 언어입니다. 2. PHP 코드는 HTML에 포함되어 웹 페이지를 쉽게 개발할 수 있습니다. 3. PHP는 서버 측 로직을 처리하고 HTML 출력을 생성하며 사용자 상호 작용 및 데이터 처리를 지원합니다. 4. PHP는 데이터베이스와 상호 작용하고 프로세스 양식 제출 및 서버 측 작업을 실행할 수 있습니다.

PHP 및 웹 : 장기적인 영향 탐색PHP 및 웹 : 장기적인 영향 탐색Apr 16, 2025 am 12:17 AM

PHP는 지난 수십 년 동안 네트워크를 형성했으며 웹 개발에서 계속 중요한 역할을 할 것입니다. 1) PHP는 1994 년에 시작되었으며 MySQL과의 원활한 통합으로 인해 개발자에게 최초의 선택이되었습니다. 2) 핵심 기능에는 동적 컨텐츠 생성 및 데이터베이스와의 통합이 포함되며 웹 사이트를 실시간으로 업데이트하고 맞춤형 방식으로 표시 할 수 있습니다. 3) PHP의 광범위한 응용 및 생태계는 장기적인 영향을 미쳤지 만 버전 업데이트 및 보안 문제에 직면 해 있습니다. 4) PHP7의 출시와 같은 최근 몇 년간의 성능 향상을 통해 현대 언어와 경쟁 할 수 있습니다. 5) 앞으로 PHP는 컨테이너화 및 마이크로 서비스와 같은 새로운 도전을 다루어야하지만 유연성과 활발한 커뮤니티로 인해 적응력이 있습니다.

PHP를 사용하는 이유는 무엇입니까? 설명 된 장점과 혜택PHP를 사용하는 이유는 무엇입니까? 설명 된 장점과 혜택Apr 16, 2025 am 12:16 AM

PHP의 핵심 이점에는 학습 용이성, 강력한 웹 개발 지원, 풍부한 라이브러리 및 프레임 워크, 고성능 및 확장 성, 크로스 플랫폼 호환성 및 비용 효율성이 포함됩니다. 1) 배우고 사용하기 쉽고 초보자에게 적합합니다. 2) 웹 서버와 우수한 통합 및 여러 데이터베이스를 지원합니다. 3) Laravel과 같은 강력한 프레임 워크가 있습니다. 4) 최적화를 통해 고성능을 달성 할 수 있습니다. 5) 여러 운영 체제 지원; 6) 개발 비용을 줄이기위한 오픈 소스.

See all articles

핫 AI 도구

Undresser.AI Undress

Undresser.AI Undress

사실적인 누드 사진을 만들기 위한 AI 기반 앱

AI Clothes Remover

AI Clothes Remover

사진에서 옷을 제거하는 온라인 AI 도구입니다.

Undress AI Tool

Undress AI Tool

무료로 이미지를 벗다

Clothoff.io

Clothoff.io

AI 옷 제거제

AI Hentai Generator

AI Hentai Generator

AI Hentai를 무료로 생성하십시오.

인기 기사

R.E.P.O. 에너지 결정과 그들이하는 일 (노란색 크리스탈)
1 몇 달 전By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O. 최고의 그래픽 설정
1 몇 달 전By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O. 아무도들을 수없는 경우 오디오를 수정하는 방법
1 몇 달 전By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O. 채팅 명령 및 사용 방법
1 몇 달 전By尊渡假赌尊渡假赌尊渡假赌

뜨거운 도구

안전한 시험 브라우저

안전한 시험 브라우저

안전한 시험 브라우저는 온라인 시험을 안전하게 치르기 위한 보안 브라우저 환경입니다. 이 소프트웨어는 모든 컴퓨터를 안전한 워크스테이션으로 바꿔줍니다. 이는 모든 유틸리티에 대한 액세스를 제어하고 학생들이 승인되지 않은 리소스를 사용하는 것을 방지합니다.

스튜디오 13.0.1 보내기

스튜디오 13.0.1 보내기

강력한 PHP 통합 개발 환경

맨티스BT

맨티스BT

Mantis는 제품 결함 추적을 돕기 위해 설계된 배포하기 쉬운 웹 기반 결함 추적 도구입니다. PHP, MySQL 및 웹 서버가 필요합니다. 데모 및 호스팅 서비스를 확인해 보세요.

VSCode Windows 64비트 다운로드

VSCode Windows 64비트 다운로드

Microsoft에서 출시한 강력한 무료 IDE 편집기

WebStorm Mac 버전

WebStorm Mac 버전

유용한 JavaScript 개발 도구