内容简介:本篇文章的方法论内容基本来自订阅的极客时间-李运华老师的《从0开始学架构》中的一篇文章,会结合自己的学习经验、加上以 Flink 为例来做一个总结,也为了让自己再学习其他开源项目时能够按照这样的一个方法论搞笑的深入学习。先说下自己开源项目的一些项目,开源项目最早从上个世纪开始,我知道最早的应该是 linux 项目,再到近几年大数据领域,发展非常迅速,开源给本公司带来的好处,首先是提高这家在技术界的影响力,然后如果这个项目比较受大家认可,那么这家公司的这个技术可能会成为业界的统一解决方案,就像 Hadoop
本篇文章的方法论内容基本来自订阅的极客时间-李运华老师的《从0开始学架构》中的一篇文章,会结合自己的学习经验、加上以 Flink 为例来做一个总结,也为了让自己再学习其他开源项目时能够按照这样的一个方法论搞笑的深入学习。先说下自己开源项目的一些项目,开源项目最早从上个世纪开始,我知道最早的应该是 linux 项目,再到近几年大数据领域,发展非常迅速,开源给本公司带来的好处,首先是提高这家在技术界的影响力,然后如果这个项目比较受大家认可,那么这家公司的这个技术可能会成为业界的统一解决方案,就像 Hadoop、Kafka 等。对其他公司的好处是,节省成本、可以快速应用来解决业务中的成本。
但是对于公司技术员工不好地方是,作为这些项目的维护者,以后真的可能变成运维工程师,社区这些项目发展一般都很快,小厂很难有足够的人力、能力在这上面做太多的研发,一般都是跟着社区升级,可能发展到最后的结果是: 项目的研发由社区(或者背后主导的公司)来负责,其他公司融入这一生态,做好运维工作或产品化的东西就可以了,稳定性、可靠性、新功能交给社区,随着项目逐渐庞大,到最后可能其他公司很少有人能对这些项目有较强的掌控力,研发完全依赖于社区。这些开源项目的接口变得越来越简单、内部越来越复杂时,虽然降低了开发者、维护者的门槛,但也降低对开发者、维护者的要求,这是一把双刃剑,对于在技术上对自己有一定要求的工程师,不应该仅限于使用、原理等。
上面是一些浅薄的想法,下面开始结合李运华老师的文章总结学习开源项目的方法论。首先在学习开源项目时,有几点需要明确的是:
- 先树立正确观念,不管你是什么身份,都可以从开源项目中学到很多东西(比如:要学习 Redis 的网络模型,不需要我们成为 Redis 的开发者,也不需要一定要用到 Redis,只需要具备一定的网络编程基础,再通过阅读 Redis 源码,就可以学习 Redis 这种单进程的 Reactor 模型);
- 不要只盯着数据结构和算法,这些在学习开源项目时并没有那么重要(Nginx 使用红黑树来管理定时器,对于大多数人只需要这一点就足够了,并不需要研究 Nginx 实现红黑树的源码是如何写的,除非需要修改这部分的逻辑代码);
- 采取 自顶向下 的学习方法,源码不是第一步,而是最后一步(基本掌握了功能、原理、关键设计之后再去看源码,看源码的主要目的是为了学习其代码的写作方式以及关键技术的实现)。
例如,Rdis 的 RDB 持久化模式「会将当前内存中的数据库快照保存到磁盘文件中」,那这里所谓的 “数据库快照” 到底是怎么做的呢?在 Linux 平台上其实就是 fork 一个子进程保存就可以了;那为何 fork 子进程就生成了数据库快照了呢?这又和 Linux 的父子进程机制以及 copy-on-write 技术相关了。通过这种学习方式,既能够快速掌握系统设计的关键点(Redis 和 RDB 模式),又能够掌握具体的变成技巧(内存快照)。
下面来看下李运华老师的『自顶向下』的学习方法和步骤。
1. 安装
这里的安装并不是对着手册执行一下命令,而是要通过安装过程,获取到如下一些关键的信息:
- 这个系统的依赖组件,而依赖的组件又是系统设计和实现的基础;
- 安装目录也能够提供一些使用和运行的基本信息;
- 系统提供了哪些 工具 方便我们使用( 带着问题去学习效率是最高的 )。
以 Nginx 为例,源码安装时依赖的库有 pcre、pcre-devel、openssl、openssl-devel、zlib,光从名字上看能够了解一些信息,例如 openssl 可能和 https 有关,zlib 可能与压缩有关。再以 Memcache 为例,它最大的依赖库就是 libevent,而根据 libevent 是一个高性能的网络库,大概能够推测 Memcache 的网络实现应该是 Reactor 模型。
例如,flink1.5.0安装完成后,目录如下:
[XXX@matt@pro flink-1.5.0]$ ll total 52 drwxr-xr-x 2 XXX XXX 4096 Jul 9 23:39 bin drwxr-xr-x 2 XXX XXX 4096 Jul 9 23:57 conf drwxr-xr-x 6 XXX XXX 4096 Jul 9 23:39 examples drwxr-xr-x 2 XXX XXX 4096 Jul 9 23:39 lib -rw-r--r-- 1 XXX XXX 18197 Jul 9 23:39 LICENSE drwxr-xr-x 2 XXX XXX 4096 Jul 9 23:57 log -rw-r--r-- 1 XXX XXX 779 Jul 9 23:39 NOTICE drwxr-xr-x 2 XXX XXX 4096 Jul 9 23:39 opt -rw-r--r-- 1 XXX XXX 1308 Jul 9 23:39 README.txt
上面 bin 是运行程序,conf 是配置文件的目录,lib 和 opt 是依赖的相关 jar 包,但为什么分为两个目录去放,这个我还不是很明白。下面是目录的详细内容:
[XXX@matt@pro flink-1.5.0]$ ll bin/ total 116 -rwxr-xr-x 1 XXX XXX 23957 Jul 9 23:39 config.sh -rwxr-xr-x 1 XXX XXX 2224 Jul 9 23:39 flink -rwxr-xr-x 1 XXX XXX 1271 Jul 9 23:39 flink.bat -rwxr-xr-x 1 XXX XXX 2823 Jul 9 23:39 flink-console.sh -rwxr-xr-x 1 XXX XXX 6407 Jul 9 23:39 flink-daemon.sh -rwxr-xr-x 1 XXX XXX 1482 Jul 9 23:39 historyserver.sh -rwxr-xr-x 1 XXX XXX 2652 Jul 9 23:39 jobmanager.sh -rwxr-xr-x 1 XXX XXX 1802 Jul 9 23:39 mesos-appmaster-job.sh -rwxr-xr-x 1 XXX XXX 1971 Jul 9 23:39 mesos-appmaster.sh -rwxr-xr-x 1 XXX XXX 2013 Jul 9 23:39 mesos-taskmanager.sh -rwxr-xr-x 1 XXX XXX 1164 Jul 9 23:39 pyflink.bat -rwxr-xr-x 1 XXX XXX 1107 Jul 9 23:39 pyflink.sh -rwxr-xr-x 1 XXX XXX 1182 Jul 9 23:39 pyflink-stream.sh -rwxr-xr-x 1 XXX XXX 3434 Jul 9 23:39 sql-client.sh -rwxr-xr-x 1 XXX XXX 3364 Jul 9 23:39 start-cluster.bat -rwxr-xr-x 1 XXX XXX 1836 Jul 9 23:39 start-cluster.sh -rwxr-xr-x 1 XXX XXX 2960 Jul 9 23:39 start-scala-shell.sh -rwxr-xr-x 1 XXX XXX 1854 Jul 9 23:39 start-zookeeper-quorum.sh -rwxr-xr-x 1 XXX XXX 1616 Jul 9 23:39 stop-cluster.sh -rwxr-xr-x 1 XXX XXX 1845 Jul 9 23:39 stop-zookeeper-quorum.sh -rwxr-xr-x 1 XXX XXX 3543 Jul 9 23:39 taskmanager.sh -rwxr-xr-x 1 XXX XXX 1674 Jul 9 23:39 yarn-session.sh -rwxr-xr-x 1 XXX XXX 2281 Jul 9 23:39 zookeeper.sh [XXX@matt@pro flink-1.5.0]$ ll lib/ total 88972 -rw-r--r-- 1 XXX XXX 90458504 Jul 9 23:39 flink-dist_2.11-1.5.0.jar -rw-r--r-- 1 XXX XXX 142041 Jul 9 23:39 flink-python_2.11-1.5.0.jar -rw-r--r-- 1 XXX XXX 489884 Jul 9 23:39 log4j-1.2.17.jar -rw-r--r-- 1 XXX XXX 8870 Jul 9 23:39 slf4j-log4j12-1.7.7.jar [XXX@matt@pro flink-1.5.0]$ ll opt/ total 193956 -rw-r--r-- 1 XXX XXX 48215 Jul 9 23:39 flink-avro-1.5.0.jar -rw-r--r-- 1 XXX XXX 124115 Jul 9 23:39 flink-cep_2.11-1.5.0.jar -rw-r--r-- 1 XXX XXX 49235 Jul 9 23:39 flink-cep-scala_2.11-1.5.0.jar -rw-r--r-- 1 XXX XXX 630006 Jul 9 23:39 flink-gelly_2.11-1.5.0.jar -rw-r--r-- 1 XXX XXX 759288 Jul 9 23:39 flink-gelly-scala_2.11-1.5.0.jar -rw-r--r-- 1 XXX XXX 21140 Jul 9 23:39 flink-json-1.5.0.jar -rw-r--r-- 1 XXX XXX 16835 Jul 9 23:39 flink-metrics-datadog-1.5.0.jar -rw-r--r-- 1 XXX XXX 136599 Jul 9 23:39 flink-metrics-dropwizard-1.5.0.jar -rw-r--r-- 1 XXX XXX 278137 Jul 9 23:39 flink-metrics-ganglia-1.5.0.jar -rw-r--r-- 1 XXX XXX 161637 Jul 9 23:39 flink-metrics-graphite-1.5.0.jar -rw-r--r-- 1 XXX XXX 89072 Jul 9 23:39 flink-metrics-prometheus-1.5.0.jar -rw-r--r-- 1 XXX XXX 6029 Jul 9 23:39 flink-metrics-slf4j-1.5.0.jar -rw-r--r-- 1 XXX XXX 7712 Jul 9 23:39 flink-metrics-statsd-1.5.0.jar -rw-r--r-- 1 XXX XXX 27197071 Jul 9 23:39 flink-ml_2.11-1.5.0.jar -rw-r--r-- 1 XXX XXX 17916 Jul 9 23:39 flink-queryable-state-runtime_2.11-1.5.0.jar -rw-r--r-- 1 XXX XXX 30676687 Jul 9 23:39 flink-s3-fs-hadoop-1.5.0.jar -rw-r--r-- 1 XXX XXX 38244766 Jul 9 23:39 flink-s3-fs-presto-1.5.0.jar -rw-r--r-- 1 XXX XXX 18517471 Jul 9 23:39 flink-sql-client-1.5.0.jar -rw-r--r-- 1 XXX XXX 37325999 Jul 9 23:39 flink-streaming-python_2.11-1.5.0.jar -rw-r--r-- 1 XXX XXX 26088550 Jul 9 23:39 flink-swift-fs-hadoop-1.5.0.jar -rw-r--r-- 1 XXX XXX 18172108 Jul 9 23:39 flink-table_2.11-1.5.0.jar [XXX@matt@pro flink-1.5.0]$ ll conf/ total 56 -rw-r--r-- 1 XXX XXX 9866 Jul 9 23:57 flink-conf.yaml -rw-r--r-- 1 XXX XXX 2138 Jul 9 23:39 log4j-cli.properties -rw-r--r-- 1 XXX XXX 1884 Jul 9 23:39 log4j-console.properties -rw-r--r-- 1 XXX XXX 1939 Jul 9 23:39 log4j.properties -rw-r--r-- 1 XXX XXX 1709 Jul 9 23:39 log4j-yarn-session.properties -rw-r--r-- 1 XXX XXX 2294 Jul 9 23:39 logback-console.xml -rw-r--r-- 1 XXX XXX 2331 Jul 9 23:39 logback.xml -rw-r--r-- 1 XXX XXX 1550 Jul 9 23:39 logback-yarn.xml -rw-r--r-- 1 XXX XXX 15 Jul 9 23:39 masters -rw-r--r-- 1 XXX XXX 120 Jul 9 23:39 slaves -rw-r--r-- 1 XXX XXX 2755 Jul 9 23:39 sql-client-defaults.yaml -rw-r--r-- 1 XXX XXX 1434 Jul 9 23:39 zoo.cfg
比如这里我们想查一下 sql-client.sh
是做什么的?应该怎么使用?不同参数是什么意思,可以通过 help 信息查看。
./bin/sql-client.sh ./sql-client [MODE] [OPTIONS] The following options are available: Mode "embedded" submits Flink jobs from the local machine. Syntax: embedded [OPTIONS] "embedded" mode options: -d,--defaults <environment file> The environment properties with which every new session is initialized. Properties might be overwritten by session properties. -e,--environment <environment file> The environment properties to be imported into the session. It might overwrite default environment properties. -h,--help Show the help message with descriptions of all options. -j,--jar <JAR file> A JAR file to be imported into the session. The file might contain user-defined classes needed for the execution of statements such as functions, table sources, or sinks. Can be used multiple times. -l,--library <JAR directory> A JAR file directory with which every new session is initialized. The files might contain user-defined classes needed for the execution of statements such as functions, table sources, or sinks. Can be used multiple times. -s,--session <session identifier> The identifier for a session. 'default' is the default identifier.
2. 运行
安装完成后,我们需要真正将系统运行起来,运行系统的时候有两个地方要特别关注: 命令行和配置文件 ,它们主要提供了两个非常关键的信息:
- 系统具备哪些能力(提供哪些可配置化的参数,这些参数是做什么的以及不同的配置带来的影响是什么);
- 系统将会如何运行。
这些信息是我们窥视系统内部运行机制和原理的一扇窗口。
例如,下面 Flink 配置中一些配置参数(Flink 集群模式的安装和运行可以参考 Flink官方文档翻译:安装部署(集群模式) ),通过这几个启动时的配置参数,我们可以获取下面这些信息:
jobmanager.rpc.address jobmanager.rpc.port jobmanager.heap.mb taskmanager.numberOfTaskSlots taskmanager.numberOfTaskSlots parallelism.default
通过上面这些配置参数,我们基本上可以看到 Flink 的 Master/Salve 模型,是分为 JobManager 和 TaskManager,而 TaskManager 中又有对应的 TaskSlot,系统也提供了相应配置参数进行设置,Flink 1.5.0 的配置信息可以参考 Flink 1.5.0 配置 ,社区的文档对这些参数描述得非常清楚,如果之前有大数据系统的基础,比如了解 HDFS、YARN、Spark、Storm、Kafka 的架构,那么在看到这些参数时,其实并不会感觉到太陌生,分布式系统很多东西都是想通的。
在通常情况下,如果我们将每个命令行参数和配置项的作用和原理都全部掌握清楚了的话,基本上对系统已经很熟悉了。这里李运华老师介绍了一个他的经验,那么就是: 不管三七二十一,先把所有的配置项全部研究一遍,包括配置项的原理、作用、影响,并且尝试去修改配置项然后看看系统会有什么变化 。
3. 原理研究
在完成前两个步骤后,我们对系统已经有了初步的感觉和理解,此时可以更进一步去研究其原理。其实在研究命令行和配置项的时候已经涉及一部分原理了,但是并不是很系统,因此我们要专门针对原理进行系统性的研究。这里的关键就是 系统性 三个字,怎么才算系统性呢?主要体现在如下几个方面:
3.1. 关键特性的基本实现原理
每个应用广泛的开源项目之所以能够受到大众的欢迎,肯定是有一些卖点的,有一些它们的应用场景,常见的有高性能、高可用、可扩展等特性,那到底这些项目是如何做到其所宣称的那么牛的呢?这些牛的地方就是我们需要深入学习的地方:
- Memcache 的高性能具体是怎么做到的呢?首先是基于 libevent 实现了高性能的网络模型,其次是内存管理 Slab Allocator 机制。为了彻底理解 Memcache 的高性能网络模型,我们需要掌握很多知识:多路复用、Linux epoll、Reactor 模型、多线程等,通过研究 Memcache 的高性能网络模型,我们能够学习一个具体的项目中如何将这些东西全部串起来实现了高性能。
- 再以 React 为例,Virtual DOM 的实现原理是什么、为何要实现 Virtual DOM、React 是如何构建 Virtual DOM 树、Virtual DOM 与 DOM 什么关系等,通过研究学习 Virtual DOM,即使不使用 React,我们也能够学习如何写出高性能的前端的代码。
- 这里再以 Kafka 为例,Kafka 作为在大数据领域应用非常广泛的消息队列,它是如何实现它宣称的高性能的、高可靠?以后在0.11.0之后的版本它是如何实现幂等性、事务性的?在2.0之后是如何实现可以支撑千台机器、百万 partition 规模的?通过深入学习一些,能够让我们学学习到大数据存储系统的可靠性、高性能实现方案,已经分布式一致性(事务性)的实现;
- 最后以 Flink 为例,Flink 最开始的卖点是 Exactly once 和低延迟,现在的话再加上流式系统 SQL 的支持,那么它与 Storm、Spark streaming 相比,Flink 的 Exactly once 是怎么实现的?为什么 Storm 在现有机制上(不含 Trident)无法实现 Exactly once?Spark Streaming 微批处理模型延迟消耗主要在什么地方?为什么 Flink 可以做到低延迟?Flink 怎么实现窗口计算以及 Flink SQL 是怎么实现的,以及 Flink SQL 现有的问题的是什么?
3.2. 优缺点对比分析
这是我想特别强调的一点, 只有清楚掌握技术方案的优缺点后才算真正的掌握这门技术,也只有掌握了技术方案的优缺点后才能在架构设计的时候做出合理的选择。优缺点主要通过对比来分析,即:我们将两个类似的系统进行对比,看看它们的实现差异,以及不同的实现优缺点都是什么 。
- 典型的对比有 Memcache 和 Redis,例如(仅举例说明,实际上对比的点很多),Memcache 用多线程,Redis 用单进程,各有什么优缺点?Memcache 和 Redis 的集群方式,各有什么优缺点?
- 即使是 Redis 自身,我们也可以对比 RDB 和 AOF 两种模式的优缺点。
3.3. 如何系统性学习一个开源项目
在你了解了什么是【系统性】后,我来介绍一下原理研究的手段,主要有三种:
- 通读项目的设计文档:例如 Kafka 的设计文档,基本涵盖了消息队列设计的关键决策部分;Disruptor 的设计白皮书,详细的阐述了 Java 单机高性能的设计技巧(官方文档是学习一个项目的必须资料)。
- 阅读网上已有的分析文档:通常情况下比较热门的开源项目,都已经有非常多的分析文档了,我们可以站在前人的基础上,避免大量的重复投入。但需要注意的是,由于经验、水平、关注点等差异,不同的人分析的结论可能有差异,甚至有的是错误的,因此不能完全参照。一个比较好的方式就是多方对照,也就是说看很多篇分析文档,比较它们的内容共同点和差异点(网上分析文档很多,但是要知道如何区分这些分析文档,多对比一些,同一个东西,每个人的理解并不一定相同)。
- Demo 验证:如果有些技术点难以查到资料,自己又不确定,则可以真正去写 Demo 进行验证,通过打印一些日志或者调试,能清晰的理解具体的细节。例如,写一个简单的分配内存程序,然后通过日志和命令行(jmap、jstat、jstack 等)来查看 Java 虚拟机垃圾回收时的具体表现(开源项目一般都会有一些实例供我们学习参考,这也是我们学习一个项目的重要资料,先去看如何使用,再去看不同使用方式背后的原理)。
4. 测试
通常情况下,如果你真的准备在实际项目中使用某个开源项目的话,必须进行测试。有的同学可能会说,网上的分析和测试文档很多,直接找一篇看就可以了?如果只是自己学习和研究,这样做是可以的,因为构建完整的测试用例既需要耗费较多时间,又需要较多机器资源,如果每个项目都这么做的话,投入成本有点大;但如果是要在实践项目中使用,必须自己进行测试,因为网上搜的测试结果,不一定与自己的业务场景很契合,如果简单参考别人的测试结果,很可能会得出错误的结论。例如,开源系统的版本不同,测试结果可能差异较大。同样是 K-V 存储,别人测试的 value 是 128 字节,而你的场景 value 都达到了 128k 字节,两者的测试结果也差异很大,不能简单照搬(在实际真正使用前,需要足够的性能测试,而且要能分析出测试结论背后的理论原因,如果找不到理论做为支撑,这样的测试并不是可信的,因为网络中环境有很大的随机性)。
测试阶段需要特别强调的一点就是:测试一定要在原理研究之后做,不能安装完成立马就测试!原因在于如果对系统不熟悉,很可能出现命令行、配置参数没用对,或者运行模式选择不对,导致没有根据业务的特点搭建正确的环境、没有设计合理的测试用例,从而使得最终的测试结果得出了错误结论,误导了设计决策。曾经有团队安装完成 MySQL 5.1 后就进行性能测试,测试结果出来让人大跌眼镜,经过定位才发现 innodb_buffer_pool_size
使用的是默认值 8M。
5. 源码研究
源码研究的主要目的是 学习原理背后的具体编码如何实现,通过学习这些技巧来提升我们自己的技术能力 。例如 Redis 的 RDB 快照、Nginx 的多 Reactor 模型、Disruptor 如何使用 volatile 以及 CAS 来做无锁设计、Netty 的 Zero-Copy 等,这些技巧都很精巧,掌握后能够大大提升自己的编码能力。
通常情况下,不建议通读所有源码,因为想掌握每行代码的含义和作用还是非常耗费时间的,尤其是 MySQL、Nginx 这种规模的项目,即使是他们的开发人员,都不一定每个人都掌握了所有代码。带着明确目的去研究源码,做到有的放矢,才能事半功倍,这也是源码研究要放在最后的原因。
当然,如果是该系统的开发者,那么要求就完全不一样的,比如你在公司负责维护 Kafka,第一年的时候刚入行,对 kafka 的源码不熟悉很正常,但是一年之后,还是这样,那么其实作为 kafka 方向的开发工程师,你是不合格的。虽然不熟悉源码,也能把一些功能性的东西做好,但是如果工程师对自己没有高要求,未来在技术很难走到太远,至少在技术上很难太大的突破,当然,大部分情况下,这并不影响你的职业发展。
时间分配
前面介绍的【自顶向下】五个步骤,完整执行下来需要花费较长时间,而时间又是大部分技术人员比较稀缺的资源。很多人在学习技术的时候都会反馈说时间不够,版本进度很紧,很难有大量的时间进行学习,但如果不学习感觉自己又很难提升?面对这种两难问题,具体该如何做呢?
通常情况下,以上 5 个步骤的前 3 个步骤,不管是已经成为架构师的技术人员,还是立志成为架构师的技术人员,在研究开源项目的时候都必不可少;第四步可以在准备采用开源项目的时候才实施,第五步可以根据你的时间来进行灵活安排。这里的“灵活安排”不是说省略不去做,而是在自己有一定时间和精力的时候做,因为只有这样才能真正理解和学到具体的技术。
如果感觉自己时间和精力不够,与其蜻蜓点水每个开源项目都去简单了解一下,还不如集中精力将一个开源项目研究通透,就算是每个季度只学习一个开源项目,积累几年后这个数量也是很客观的;而且一旦你将一个项目研究透以后,再去研究其他类似项目,你会发现自己学习的非常快,因为共性的部分你已经都掌握了,只需要掌握新项目差异的部分即可。
这里个人的感想是,对于初中级工程师,最好还是要有2-3项目或者2-3个方向需要走到第五步,毕竟我们是靠这个吃饭的,对于其他的项目(目前业界统一的解决方案,比如 hdfs、hbase、spark 等),至少需要走到第四步,这样与这方面的专业人士沟通交流时,至少让自己不至于处在懵逼状态。当然对于这些项目的核心代码,也是可以深入学习,比如 spark 的 shuffle 在代码上是如何实现的等。在一个项目上深入之后,再去看同一个领域的其他项目时,当看到其他的架构时,其实我们基本上就可以清楚这架构设计的原因、要解决的问题以及这种设计带来的其他问题,每种设计都有其应用场景,比如对 Kafka 有了深入了解后,再看 RocketMQ、phxqueue 时,看到它们的架构方案基本上就明白要解决的问题以及其特定的应用场景,当然对一些独特的特性,还是需要深入到代码层面去学习的,比如 RocketMQ 的延迟队列实现。这是一些个人的感想,并不一定对,大家可以共同交流,总之,是感觉李运华老师这篇文章是值得总结分享的,希望自己在后面学习开源项目时,能够静下心、认真坚持学下去。
以上所述就是小编给大家介绍的《如何学习开源项目》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 微服务开源项目ServiceComb 毕业成为Apache顶级项目
- Facebook 2018 年度开源回顾:新增开源项目 153 个
- 欧盟向 14 个开源项目提供 Bug 悬赏,多是“老牌”项目
- 开源一个文本分析项目
- 开源存储项目知多少
- Flutter 必备开源项目
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
JS 压缩/解压工具
在线压缩/解压 JS 代码
HEX HSV 转换工具
HEX HSV 互换工具