搜索
首页后端开发php教程深入理解php中的ini配置(2),深入理解ini_PHP教程

深入理解php中的ini配置(2),深入理解ini

继续接着上一篇写。

1,运行时改变配置

在前一篇中曾经谈到,ini_set函数可以在php执行的过程中,动态修改php的部分配置。注意,仅仅是部分,并非所有的配置都可以动态修改。关于ini配置的可修改性,参见:http://php.net/manual/zh/configuration.changes.modes.php

我们直接进入ini_set的实现,函数虽然有点长,但是逻辑很清晰:

<span>PHP_FUNCTION(ini_set)
{
    </span><span>char</span> *varname, *<span>new_value;
    </span><span>int</span><span> varname_len, new_value_len;
    </span><span>char</span> *<span>old_value;

    </span><span>if</span> (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, <span>"</span><span>ss</span><span>"</span>, &varname, &varname_len, &new_value, &new_value_len) ==<span> FAILURE) {
        </span><span>return</span><span>;
    }

    </span><span>//</span><span> 去EG(ini_directives)中获取配置的值</span>
    old_value = zend_ini_string(varname, varname_len + <span>1</span>, <span>0</span><span>);

    </span><span>/*</span><span> copy to return here, because alter might free it! </span><span>*/</span>
    <span>if</span><span> (old_value) {
        RETVAL_STRING(old_value, </span><span>1</span><span>);
    } </span><span>else</span><span> {
        RETVAL_FALSE;
    }

    </span><span>//</span><span> 如果开启了安全模式,那么如下这些ini配置可能涉及文件操作,需要要辅助检查uid</span>
<span>#define</span> _CHECK_PATH(var, var_len, ini) php_ini_check_path(var, var_len, ini, sizeof(ini))
    <span>/*</span><span> safe_mode & basedir check </span><span>*/</span>
    <span>if</span> (PG(safe_mode) ||<span> PG(open_basedir)) {
        </span><span>if</span> (_CHECK_PATH(varname, varname_len, <span>"</span><span>error_log</span><span>"</span>) ||<span>
            _CHECK_PATH(varname, varname_len, </span><span>"</span><span>java.class.path</span><span>"</span>) ||<span>
            _CHECK_PATH(varname, varname_len, </span><span>"</span><span>java.home</span><span>"</span>) ||<span>
            _CHECK_PATH(varname, varname_len, </span><span>"</span><span>mail.log</span><span>"</span>) ||<span>
            _CHECK_PATH(varname, varname_len, </span><span>"</span><span>java.library.path</span><span>"</span>) ||<span>
            _CHECK_PATH(varname, varname_len, </span><span>"</span><span>vpopmail.directory</span><span>"</span><span>)) {
            </span><span>if</span> (PG(safe_mode) && (!<span>php_checkuid(new_value, NULL, CHECKUID_CHECK_FILE_AND_DIR))) {
                zval_dtor(return_value);
                RETURN_FALSE;
            }
            </span><span>if</span><span> (php_check_open_basedir(new_value TSRMLS_CC)) {
                zval_dtor(return_value);
                RETURN_FALSE;
            }
        }
    }

    </span><span>//</span><span> 在安全模式下,如下这些ini受到保护,不会被动态修改</span>
    <span>if</span><span> (PG(safe_mode)) {
        </span><span>if</span> (!strncmp(<span>"</span><span>max_execution_time</span><span>"</span>, varname, <span>sizeof</span>(<span>"</span><span>max_execution_time</span><span>"</span>)) ||
            !strncmp(<span>"</span><span>memory_limit</span><span>"</span>, varname, <span>sizeof</span>(<span>"</span><span>memory_limit</span><span>"</span>)) ||
            !strncmp(<span>"</span><span>child_terminate</span><span>"</span>, varname, <span>sizeof</span>(<span>"</span><span>child_terminate</span><span>"</span><span>))
        ) {
            zval_dtor(return_value);
            RETURN_FALSE;
        }
    }

    </span><span>//</span><span> 调用zend_alter_ini_entry_ex去动态修改ini配置</span>
    <span>if</span> (zend_alter_ini_entry_ex(varname, varname_len + <span>1</span>, new_value, new_value_len, PHP_INI_USER, PHP_INI_STAGE_RUNTIME, <span>0</span> TSRMLS_CC) ==<span> FAILURE) {
        zval_dtor(return_value);
        RETURN_FALSE;
    }
}</span>

可以看到,除了一些必要的验证工作,主要就是调用zend_alter_ini_entry_ex。

我们继续跟进到zend_alter_ini_entry_ex函数中:

ZEND_API <span>int zend_alter_ini_entry_ex(<span>char *name, <span>uint name_length, <span>char *new_value, <span>uint new_value_length, <span>int modify_type, <span>int stage, <span>int force_change TSRMLS_DC) <span>/*<span> {{{ <span>*/<span>
{
    zend_ini_entry *<span>ini_entry;
    <span>char *<span>duplicate;
    zend_bool modifiable;
    zend_bool modified;

    <span>//<span> 找出EG(ini_directives)中对应的ini_entry
    <span>if (zend_hash_find(EG(ini_directives), name, name_length, (<span>void **) &ini_entry) ==<span> FAILURE) {
        <span>return<span> FAILURE;
    }

    <span>//<span> 是否被修改以及可修改性
    modifiable = ini_entry-><span>modifiable;
    modified = ini_entry-><span>modified;

    <span>if (stage == ZEND_INI_STAGE_ACTIVATE && modify_type ==<span> ZEND_INI_SYSTEM) {
        ini_entry->modifiable =<span> ZEND_INI_SYSTEM;
    }

    <span>//<span> 是否强制修改
    <span>if (!<span>force_change) {
        <span>if (!(ini_entry->modifiable &<span> modify_type)) {
            <span>return<span> FAILURE;
        }
    }

    <span>//<span> EG(modified_ini_directives)用于存放被修改过的ini_entry<br />    // 主要用做恢复
    <span>if (!<span>EG(modified_ini_directives)) {
        ALLOC_HASHTABLE(EG(modified_ini_directives));
        zend_hash_init(EG(modified_ini_directives), <span>8, NULL, NULL, <span>0<span>);
    }
    
    <span>//<span> 将ini_entry中的值,值的长度,可修改范围,保留到orig_xxx中去<br />    // 以便在请求结束的时候,可以对ini_entry做恢复<br /><span><span>    <span>if (!<span>modified) {
        ini_entry->orig_value = ini_entry-><span>value;
        ini_entry->orig_value_length = ini_entry-><span>value_length;
        ini_entry->orig_modifiable =<span> modifiable;
        ini_entry->modified = <span>1<span>;
        zend_hash_add(EG(modified_ini_directives), name, name_length, &ini_entry, <span>sizeof(zend_ini_entry*<span>), NULL);
    }

    duplicate =<span> estrndup(new_value, new_value_length);

    <span>//<span> 调用modify来更新XXX_G中对应的ini配置<span><span>
    <span>if (!ini_entry->on_modify || ini_entry->on_modify(ini_entry, duplicate, new_value_length, ini_entry->mh_arg1, ini_entry->mh_arg2, ini_entry->mh_arg3, stage TSRMLS_CC) ==<span> SUCCESS) {
        <span>//<span> 同上面,如果多次修改,则需要释放前一次修改的值
        <span>if (modified && ini_entry->orig_value != ini_entry->value) {<span>
            efree(ini_entry-><span>value);
        }
        ini_entry->value =<span> duplicate;
        ini_entry->value_length =<span> new_value_length;
    } <span>else<span> {
        efree(duplicate);
        <span>return<span> FAILURE;
    }

    <span>return<span> SUCCESS;
}</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>

有3处逻辑需要我们仔细体会:

1)ini_entry中的modified字段用来表示该配置是否被动态修改过。一旦该ini配置发生修改,modified就会被置为1。上述代码中有一段很关键:

<span>//</span><span> 如果多次调用ini_set,则orig_value等始终保持最原始的值</span>
<span>if</span> (!<span>modified) {
    ini_entry</span>->orig_value = ini_entry-><span>value;
    ini_entry</span>->orig_value_length = ini_entry-><span>value_length;
    ini_entry</span>->orig_modifiable =<span> modifiable;
    ini_entry</span>->modified = <span>1</span><span>;
    zend_hash_add(EG(modified_ini_directives), name, name_length, </span>&ini_entry, <span>sizeof</span>(zend_ini_entry*<span>), NULL);
}</span>

这段代码表示,不管我们先后在php代码中调用几次ini_set,只有第一次ini_set时才会进入这段逻辑,设置好orig_value。从第二次调用ini_set开始,便不会再次执行这段分支,因为此时的modified已经被置为1了。因此,ini_entry->orig_value始终保存的是第一次修改之前的配置值(即最原始的配置)。

2)为了能使ini_set修改的配置立即生效,需要on_modify回调函数。

如前一篇文中所述,调用on_modify是为了能够更新模块的全局变量。再次回忆下,首先,模块全局变量中的配置已经不是字符串类型了,该用bool用bool、该用int用int。其次,每一个ini_entry中都存储了该模块全局变量的地址以及对应的偏移量,使得on_modify可以很迅速的进行内存修改。此外不要忘记,on_modify调用完了之后,仍需进一步更新ini_entry->value,这样EG(ini_directives)中的配置值就是最新的了。

3)这里出现了一张新的hash表,EG(modified_ini_directives)。

EG(modified_ini_directives)只用于存放被动态修改过的ini配置,如果一个ini配置被动态修改过,那么它既存在于EG(ini_directives)中,又存在于EG(modified_ini_directives)中。既然每一个ini_entry都有modified字段做标记,那岂不是可以遍历EG(ini_directives)来获得所有被修改过的配置呢?

答案是肯定的。个人觉得,这里的EG(modified_ini_directives)主要还是为了提升性能,酱直接遍历EG(modified_ini_directives)就足够了。此外,把EG(modified_ini_directives)的初始化推迟到zend_alter_ini_entry_ex中,也可以看出php在细节上的性能优化点。

2,恢复配置

ini_set的作用时间和php.ini文件的作用时间是不一样的,一旦请求执行结束,则ini_set会失效。此外,当我们代码中调用了ini_restore函数,则之前通过ini_set设置的配置也会失效。

每一个php请求执行完毕之后,会触发php_request_shutdown,它和php_request_startup是两个相对应过程。如果php是挂接在apache/nginx下,则每处理完一个http请求,就会调用php_request_shutdown;如果php以CLI模式来运行,则脚本执行完毕之后,也会调用php_request_shutdown。

在php_request_shutdown中,我们可以看到针对ini的恢复处理:

<span>/*</span><span> 7. Shutdown scanner/executor/compiler and restore ini entries </span><span>*/</span><span>
zend_deactivate(TSRMLS_C);</span>

进入zend_deactivate,可以进一步看到调用了zend_ini_deactivate函数,由zend_ini_deactivate来负责将php的配置进行恢复。

<span>zend_try {
    zend_ini_deactivate(TSRMLS_C);
} zend_end_try();</span>

具体来看看zend_ini_deactivate的实现:

ZEND_API <span>int</span> zend_ini_deactivate(TSRMLS_D) <span>/*</span><span> {{{ </span><span>*/</span><span>
{
    </span><span>if</span><span> (EG(modified_ini_directives)) {
        </span><span>//</span><span> 遍历EG(modified_ini_directives)中这张表
        </span><span>//</span><span> 对每一个ini_entry调用zend_restore_ini_entry_wrapper</span>
<span>        zend_hash_apply(EG(modified_ini_directives), (apply_func_t) zend_restore_ini_entry_wrapper TSRMLS_CC);
        
        </span><span>//</span><span> 回收操作</span>
<span>        zend_hash_destroy(EG(modified_ini_directives));
        FREE_HASHTABLE(EG(modified_ini_directives));
        EG(modified_ini_directives) </span>=<span> NULL;
    }
    </span><span>return</span><span> SUCCESS;
}</span>

从zend_hash_apply来看,真正恢复ini的任务最终落地到了zend_restore_ini_entry_wrapper回调函数。

<span>static</span> <span>int</span> zend_restore_ini_entry_wrapper(zend_ini_entry **<span>ini_entry TSRMLS_DC)
{
    </span><span>//</span><span> zend_restore_ini_entry_wrapper就是zend_restore_ini_entry_cb的封装</span>
    zend_restore_ini_entry_cb(*<span>ini_entry, ZEND_INI_STAGE_DEACTIVATE TSRMLS_CC);
    </span><span>return</span> <span>1</span><span>;
}

</span><span>static</span> <span>int</span> zend_restore_ini_entry_cb(zend_ini_entry *ini_entry, <span>int</span><span> stage TSRMLS_DC)
{
    </span><span>int</span> result =<span> FAILURE;

    </span><span>//</span><span> 只看修改过的ini项</span>
    <span>if</span> (ini_entry-><span>modified) {
        </span><span>if</span> (ini_entry-><span>on_modify) {
            </span><span>//</span><span> 使用orig_value,对XXX_G内的相关字段进行重新设置</span>
<span>            zend_try {
                result </span>= ini_entry->on_modify(ini_entry, ini_entry->orig_value, ini_entry->orig_value_length, ini_entry->mh_arg1, ini_entry->mh_arg2, ini_entry-><span>mh_arg3, stage TSRMLS_CC);
            } zend_end_try();
        }
        </span><span>if</span> (stage == ZEND_INI_STAGE_RUNTIME && result ==<span> FAILURE) {
            </span><span>/*</span><span> runtime failure is OK </span><span>*/</span>
            <span>return</span> <span>1</span><span>;
        }
        </span><span>if</span> (ini_entry->value != ini_entry-><span>orig_value) {
            efree(ini_entry</span>-><span>value);
        }
        
        </span><span>//</span><span> ini_entry本身恢复到最原始的值</span>
        ini_entry->value = ini_entry-><span>orig_value;
        ini_entry</span>->value_length = ini_entry-><span>orig_value_length;
        ini_entry</span>->modifiable = ini_entry-><span>orig_modifiable;
        ini_entry</span>->modified = <span>0</span><span>;
        ini_entry</span>->orig_value =<span> NULL;
        ini_entry</span>->orig_value_length = <span>0</span><span>;
        ini_entry</span>->orig_modifiable = <span>0</span><span>;
    }
    </span><span>return</span> <span>0</span><span>;
}</span>

逻辑都蛮清晰的,相信读者可以看明白。总结一下关于ini配置的恢复流程:

php_request_shutdown--->zend_deactivate--->zend_ini_deactivate--->zend_restore_ini_entry_wrapper--->zend_restore_ini_entry_cb

3,配置的销毁

在sapi生命周期结束的时候,比如apache关闭,cli程序执行完毕等等。一旦进入到这个阶段,之前所说的configuration_hash,EG(ini_directives)等都需要被销毁,其用到的内存空间需要被释放。

1,php会依次结束所有的模块,在每个模块的PHP_MSHUTDOWN_FUNCTION中调用UNREGISTER_INI_ENTRIES。UNREGISTER_INI_ENTRIES和REGISTER_INI_ENTRIES对应,但是UNREGISTER_INI_ENTRIES并不负责模块全局空间的释放,XXX_globals这块内存放在静态数据区上,无需人为回收。

UNREGISTER_INI_ENTRIES主要做的事情,是将某个模块的ini_entry配置从EG(ini_directives)表中删除。删除之后,ini_entry本身的空间会被回收,但是ini_entry->value不一定会被回收。

当所有模块的PHP_MSHUTDOWN_FUNCTION都调用UNREGISTER_INI_ENTRIES一遍之后,EG(ini_directives)中只剩下了Core模块的ini配置。此时,就需要手动调用UNREGISTER_INI_ENTRIES,来完成对Core模块配置的删除工作。

<span>void</span><span> php_module_shutdown(TSRMLS_D)
{
    ...
    
    </span><span>//</span><span> zend_shutdown会依次关闭除了Core之外的所有php模块
    </span><span>//</span><span> 关闭时会调用各个模块的PHP_MSHUTDOWN_FUNCTION</span>
<span>    zend_shutdown(TSRMLS_C);
    
    ...

    </span><span>//</span><span> 至此,EG(ini_directives)中只剩下了Core模块的配置
    </span><span>//</span><span> 这里手动清理一下</span>
<span>    UNREGISTER_INI_ENTRIES();
    
    </span><span>/</span><span>/ 回收configuration_hash</span><span>
    php_shutdown_config();
<br /></span>
<span>    <span>/<span>/ 回收EG(ini_directives)<br />    </span></span>zend_ini_shutdown(TSRMLS_C);<br />
    ...
}</span>

当手动调用UNREGISTER_INI_ENTRIES完成之后,EG(ini_directives)已经不包含任何的元素,理论上讲,此时的EG(ini_directives)是一张空的hash表。

2,configuration_hash的回收发生在EG(ini_directives)之后,上面贴出的代码中有关于php_shutdown_config的函数调用。php_shutdown_config主要负责回收configuration_hash。

<span>int</span> php_shutdown_config(<span>void</span><span>)
{
    </span><span>//</span><span> 回收configuration_hash</span>
    zend_hash_destroy(&<span>configuration_hash);
    
    ...
    
    </span><span>return</span><span> SUCCESS;
}</span>

注意zend_hash_destroy并不会释放configuration_hash本身的空间,同XXX_G访问的模块全局空间一样,configuration_hash也是一个全局变量,无需手动回收。

3,当php_shutdown_config完成时,只剩下EG(ini_directives)的自身空间还没被释放。因此最后一步调用zend_ini_shutdown。zend_ini_shutdown用于释放EG(ini_directives)。在前文已经提到,此时的EG(ini_directives)理论上是一张空的hash表,因此该HashTable本身所占用的空间需要被释放。

ZEND_API <span>int</span><span> zend_ini_shutdown(TSRMLS_D)
{
    </span><span>//</span><span> EG(ini_directives)是动态分配出的空间,需要回收</span>
<span>    zend_hash_destroy(EG(ini_directives));
    free(EG(ini_directives));
    </span><span>return</span><span> SUCCESS;
}</span>

4,总结

用一张图大致描述一下和ini配置相关的流程:

 

 

php中ini的配置是怎做的

直接编辑php目录下的php.ini文件即可

有一个“register_globals = Off”值,这个值是用来打开全局变量的,比如表单送过来的值,如果这个值设为“Off”,就只能用“$_POST['变量名']、$_GET['变量名 ']”等来取得送过来的值,如果设为“On”,就可以直接使用“$变量名”来获取送过来的值,当然,设为“Off”就比较安全,不会让人轻易将网页间传送 的数据截取。这个值是否改成“On”就看自己感觉了,是安全重要还是方便重要?
要用mysql,就要把“;extension= php_mysql.dll”前的“;”去掉。所有的模块文件都放在php解压缩目录的“ext”之下,用什么就把前面的“;”去掉就行了。
 

php怎读取ini配置文件并按要显示

您看这样符合您的要求吗?
';// 遍历所有键foreach($keys[0] as $key) { // 如果值为OK,而且在第二个ini文件中不存在,则显示ok if($values[0][$key]==='OK' AND !isset($values[1][$key])) { echo '

'; echo '',$key,''; echo ' >
 

www.bkjia.comtruehttp://www.bkjia.com/PHPjc/893204.htmlTechArticle深入理解php中的ini配置(2),深入理解ini 继续接着上一篇写。 1,运行时改变 配置 在前一篇中曾经谈到, ini_set函数可以在php执行的过程中,...
声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
PHP和Python:解释了不同的范例PHP和Python:解释了不同的范例Apr 18, 2025 am 12:26 AM

PHP主要是过程式编程,但也支持面向对象编程(OOP);Python支持多种范式,包括OOP、函数式和过程式编程。PHP适合web开发,Python适用于多种应用,如数据分析和机器学习。

PHP和Python:深入了解他们的历史PHP和Python:深入了解他们的历史Apr 18, 2025 am 12:25 AM

PHP起源于1994年,由RasmusLerdorf开发,最初用于跟踪网站访问者,逐渐演变为服务器端脚本语言,广泛应用于网页开发。Python由GuidovanRossum于1980年代末开发,1991年首次发布,强调代码可读性和简洁性,适用于科学计算、数据分析等领域。

在PHP和Python之间进行选择:指南在PHP和Python之间进行选择:指南Apr 18, 2025 am 12:24 AM

PHP适合网页开发和快速原型开发,Python适用于数据科学和机器学习。1.PHP用于动态网页开发,语法简单,适合快速开发。2.Python语法简洁,适用于多领域,库生态系统强大。

PHP和框架:现代化语言PHP和框架:现代化语言Apr 18, 2025 am 12:14 AM

PHP在现代化进程中仍然重要,因为它支持大量网站和应用,并通过框架适应开发需求。1.PHP7提升了性能并引入了新功能。2.现代框架如Laravel、Symfony和CodeIgniter简化开发,提高代码质量。3.性能优化和最佳实践进一步提升应用效率。

PHP的影响:网络开发及以后PHP的影响:网络开发及以后Apr 18, 2025 am 12:10 AM

PHPhassignificantlyimpactedwebdevelopmentandextendsbeyondit.1)ItpowersmajorplatformslikeWordPressandexcelsindatabaseinteractions.2)PHP'sadaptabilityallowsittoscaleforlargeapplicationsusingframeworkslikeLaravel.3)Beyondweb,PHPisusedincommand-linescrip

PHP类型提示如何起作用,包括标量类型,返回类型,联合类型和无效类型?PHP类型提示如何起作用,包括标量类型,返回类型,联合类型和无效类型?Apr 17, 2025 am 12:25 AM

PHP类型提示提升代码质量和可读性。1)标量类型提示:自PHP7.0起,允许在函数参数中指定基本数据类型,如int、float等。2)返回类型提示:确保函数返回值类型的一致性。3)联合类型提示:自PHP8.0起,允许在函数参数或返回值中指定多个类型。4)可空类型提示:允许包含null值,处理可能返回空值的函数。

PHP如何处理对象克隆(克隆关键字)和__clone魔法方法?PHP如何处理对象克隆(克隆关键字)和__clone魔法方法?Apr 17, 2025 am 12:24 AM

PHP中使用clone关键字创建对象副本,并通过\_\_clone魔法方法定制克隆行为。1.使用clone关键字进行浅拷贝,克隆对象的属性但不克隆对象属性内的对象。2.通过\_\_clone方法可以深拷贝嵌套对象,避免浅拷贝问题。3.注意避免克隆中的循环引用和性能问题,优化克隆操作以提高效率。

PHP与Python:用例和应用程序PHP与Python:用例和应用程序Apr 17, 2025 am 12:23 AM

PHP适用于Web开发和内容管理系统,Python适合数据科学、机器学习和自动化脚本。1.PHP在构建快速、可扩展的网站和应用程序方面表现出色,常用于WordPress等CMS。2.Python在数据科学和机器学习领域表现卓越,拥有丰富的库如NumPy和TensorFlow。

See all articles

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

AI Hentai Generator

AI Hentai Generator

免费生成ai无尽的。

热门文章

R.E.P.O.能量晶体解释及其做什么(黄色晶体)
1 个月前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳图形设置
1 个月前By尊渡假赌尊渡假赌尊渡假赌
威尔R.E.P.O.有交叉游戏吗?
1 个月前By尊渡假赌尊渡假赌尊渡假赌

热工具

VSCode Windows 64位 下载

VSCode Windows 64位 下载

微软推出的免费、功能强大的一款IDE编辑器

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

MinGW - 适用于 Windows 的极简 GNU

MinGW - 适用于 Windows 的极简 GNU

这个项目正在迁移到osdn.net/projects/mingw的过程中,你可以继续在那里关注我们。MinGW:GNU编译器集合(GCC)的本地Windows移植版本,可自由分发的导入库和用于构建本地Windows应用程序的头文件;包括对MSVC运行时的扩展,以支持C99功能。MinGW的所有软件都可以在64位Windows平台上运行。

WebStorm Mac版

WebStorm Mac版

好用的JavaScript开发工具

SublimeText3 Linux新版

SublimeText3 Linux新版

SublimeText3 Linux最新版