首页 文章 精选 留言 我的

精选列表

搜索[源码阅读],共10000篇文章
优秀的个人博客,低调大师

Spark源码分析 – SparkEnv

SparkEnv在两个地方会被创建, 由于SparkEnv中包含了很多重要的模块, 比如BlockManager, 所以SparkEnv很重要 Driver端, 在SparkContext初始化的时候, SparkEnv会被创建 // Create the Spark execution environment (cache, map output tracker, etc) private[spark] val env = SparkEnv.createFromSystemProperties( "<driver>", // 表示是driver, 下面的executor则是executorid System.getProperty("spark.driver.host"), System.getProperty("spark.driver.port").toInt, true, isLocal) SparkEnv.set(env) Executor端, 在executor初始化时被创建 // Initialize Spark environment (using system properties read above) val env = SparkEnv.createFromSystemProperties(executorId, slaveHostname, 0, false, false) SparkEnv.set(env) SparkEnv Class 用于hold所有Spark运行时的环境对象, serializer, Akka actor system, block manager, and map output tracker等 /** * Holds all the runtime environment objects for a running Spark instance (either master or worker), * including the serializer, Akka actor system, block manager, map output tracker, etc. Currently * Spark code finds the SparkEnv through a thread-local variable, so each thread that accesses these * objects needs to have the right SparkEnv set. You can get the current environment with * SparkEnv.get (e.g. after creating a SparkContext) and set it with SparkEnv.set. */ class SparkEnv ( val executorId: String, val actorSystem: ActorSystem, val serializerManager: SerializerManager, val serializer: Serializer, val closureSerializer: Serializer, val cacheManager: CacheManager, val mapOutputTracker: MapOutputTracker, val shuffleFetcher: ShuffleFetcher, val broadcastManager: BroadcastManager, val blockManager: BlockManager, val connectionManager: ConnectionManager, val httpFileServer: HttpFileServer, val sparkFilesDir: String, val metricsSystem: MetricsSystem) { } SparkEnv Object scala使用伴生object当作类接口 除了基本的get和set 就是在createFromSystemProperties中创建了一堆很关键的对象 object SparkEnv extends Logging { private val env = new ThreadLocal[SparkEnv] // ThreadLocal,所以每个线程各访问各的 @volatile private var lastSetSparkEnv : SparkEnv = _ // 缓存最新更新的SparkEnv,并且volatile,便于其他线程获得 def set(e: SparkEnv) { lastSetSparkEnv = e env.set(e) } /** * Returns the ThreadLocal SparkEnv, if non-null. Else returns the SparkEnv * previously set in any thread. */ def get: SparkEnv = { Option(env.get()).getOrElse(lastSetSparkEnv) // 没有local时, 可以用lastSetSparkEnv } /** * Returns the ThreadLocal SparkEnv. */ def getThreadLocal : SparkEnv = { env.get() // 只取到local的 } def createFromSystemProperties( executorId: String, hostname: String, port: Int, isDriver: Boolean, isLocal: Boolean): SparkEnv = { val (actorSystem, boundPort) = AkkaUtils.createActorSystem("spark", hostname, port) val classLoader = Thread.currentThread.getContextClassLoader // Create an instance of the class named by the given Java system property, or by // defaultClassName if the property is not set, and return it as a T def instantiateClass[T](propertyName: String, defaultClassName: String): T = { val name = System.getProperty(propertyName, defaultClassName) Class.forName(name, true, classLoader).newInstance().asInstanceOf[T] } val serializerManager = new SerializerManager val serializer = serializerManager.setDefault( System.getProperty("spark.serializer", "org.apache.spark.serializer.JavaSerializer")) val closureSerializer = serializerManager.get( System.getProperty("spark.closure.serializer", "org.apache.spark.serializer.JavaSerializer")) val connectionManager = blockManager.connectionManager val broadcastManager = new BroadcastManager(isDriver) val cacheManager = new CacheManager(blockManager) // BlockManager val blockManagerMaster = new BlockManagerMaster(registerOrLookup( // registerOrLookup表示只有在master上创建Actor对象, slave上只是创建ref "BlockManagerMaster", new BlockManagerMasterActor(isLocal))) val blockManager = new BlockManager(executorId, actorSystem, blockManagerMaster, serializer) // MapOutputTracker val mapOutputTracker = new MapOutputTracker() mapOutputTracker.trackerActor = registerOrLookup( // 同样只有在master创建actor对象 "MapOutputTracker", new MapOutputTrackerActor(mapOutputTracker)) // ShuffleFetcher val shuffleFetcher = instantiateClass[ShuffleFetcher]( "spark.shuffle.fetcher", "org.apache.spark.BlockStoreShuffleFetcher") val httpFileServer = new HttpFileServer() httpFileServer.initialize() System.setProperty("spark.fileserver.uri", httpFileServer.serverUri) val metricsSystem = if (isDriver) { MetricsSystem.createMetricsSystem("driver") } else { MetricsSystem.createMetricsSystem("executor") } metricsSystem.start() new SparkEnv( executorId, actorSystem, serializerManager, serializer, closureSerializer, cacheManager, mapOutputTracker, shuffleFetcher, broadcastManager, blockManager, connectionManager, httpFileServer, sparkFilesDir, metricsSystem) } } 本文章摘自博客园,原文发布日期:2014-01-13

优秀的个人博客,低调大师

Android AsyncTask 源码解析

AsyncTask中的几个方法才能完成对任务的定制。经常需要去重写的方法有以下四个: 1.onPreExecute() 这个方法会在后台任务开始执行之间调用,用于进行一些界面上的初始化操作,比如显示一个进度条对话框等。 2.doInBackground(Params...) 这个方法中的所有代码都会在子线程中运行,我们应该在这里去处理所有的耗时任务。任务一旦完成就可以通过return语句来将任务的执行结果进行返回,如果AsyncTask的第三个泛型参数指定的是Void,就可以不返回任务执行结果。注意,在这个方法中是不可以进行UI操作的,如果需要更新UI元素,比如说反馈当前任务的执行进度,可以调用publishProgress(Progress...)方法来完成。 3.onProgressUpdate(Progress...) 当在后台任务中调

优秀的个人博客,低调大师

ElasticSearch Bulk 源码解析

本来应该先有这篇文章,后有 如何提高ElasticSearch 索引速度才对。不过当时觉得后面一篇文章会更有实际意义一些,所以先写了后面那篇文章。结果现在这篇文章晚了20多天。 前言 读这篇文章前,建议先看看ElasticSearch Rest/RPC 接口解析,有利于你把握ElasticSearch接受处理请求的脉络。对于RPC类的调用,我会在后文简单提及,只是endpoint不一样,内部处理逻辑还是一样的。这篇只会讲IndexRequest,其他如DeleteRequest,UpdateRequest之类的,我们暂时不涉及。 类处理路径 RestBulkAction -> TransportBulkAction -> TransportShardBulkAction 其中TransportShardBulkAction比较特殊,有个继承结构: TransportShardBulkAction < TransportReplicationAction < TransportAction 主入口是TransportAction,具体的业务逻辑实现分布到子类(TransportReplicationAction)和孙子类(TransportShardBulkAction)里了。 另外,我们也会提及org.elasticsearch.index.engine.Engine相关的东西,从而让大家清楚的了解ES是如何和Lucene关联上的。 RestBulkAction 入口自然是org.elasticsearch.rest.action.bulk.RestBulkAction,一个请求会构建一个BulkRequest对象,BulkRequest.add方法会解析你提交的文本。对于类型为index或者create的(还记得bulk提交的文本格式是啥样子的么?),都会被构建出IndexRequest对象,这些解析后的对象会被放到BulkRequest对象的属性requests里。当然如果是update,delete等则会构建出其他对象,但都会放到requests里。 public class BulkRequest extends ActionRequest<BulkRequest> implements CompositeIndicesRequest { //这个就是前面提到的requests final List<ActionRequest> requests = new ArrayList<>(); //这个复杂的方法就是通过http请求参数解析出 //IndexRequest,DeleteRequest,UpdateRequest等然后放到requests里 public BulkRequest add(BytesReference data, @Nullable String defaultIndex, @Nullable String defaultType, @Nullable String defaultRouting, @Nullable String[] defaultFields, @Nullable Object payload, boolean allowExplicitIndex) throws Exception { XContent xContent = XContentFactory.xContent(data); int line = 0; int from = 0; int length = data.length(); byte marker = xContent.streamSeparator(); while (true) { 接着通过NodeClient将请求发送到TransportBulkAction类(回忆下之前文章里提到的映射关系,譬如 TransportAction,两层映射关系解析 )。对应的方法如下: //这里的client其实是NodeClient client.bulk(bulkRequest, new RestBuilderListener<BulkResponse>(channel) { TransportBulkAction 看这个类的签名: public class TransportBulkAction extends HandledTransportAction<BulkRequest, BulkResponse> { 实现了HandledTransportAction,说明这个类同时也是RPC接口的逻辑处理类。如果你点进HandledTransportAction就能看到ES里经典的messageReceived方法了。这个是题外话 该类对应的入口是: protected void doExecute(final BulkRequest bulkRequest, final ActionListener<BulkResponse> listener) {这里的bulkRequest 就是前面RestBulkAction组装好的。该方法第一步是判断是不是需要自动建索引,如果索引不存在,就自动创建了。 接着通过executeBulk方法进入原来的流程。在该方法中,对bulkRequest.requests 进行了两次for循环。 第一次判定如果是IndexRequest就调用IndexRequest.process方法,主要是为了解析出timestamp,routing,id,parent 等字段。 第二次是为了对数据进行分拣。大致是为了形成这么一种结构: //这里的BulkItemRequest来源于 IndexRequest等 Map[ShardId, List[BulkItemRequest]] 接着对新形成的这个结构(ShardId -> List[BulkItemRequest])做循环,也就是针对每个ShardId里的数据进行统一处理。有了ShardId,bulkRequest,List[BulkItemRequest]等信息后,统一封装成BulkShardRequest。从名字看就很好理解,就是对属于同一ShardId的数据构建一个新的类似BulkRequest的对象。 接着就到TransportShardBulkAction,TransportReplicationAction,TransportAction 三代人出场了: //这里的shardBulkAction 是TransportShardBulkAction shardBulkAction.execute(bulkShardRequest, new ActionListener<BulkShardResponse>() { TransportReplicationAction/TransportShardBulkAction TransportAction是一个通用的主类,具体逻辑还是其子类来实现。虽然前面提到shardBulkAction是TransportShardBulkAction,但其实流程逻辑还是TransportReplicationAction来完成的。入口在该类的doExecute方法: @Override protected void doExecute(Request request, ActionListener<Response> listener) { new PrimaryPhase(request, listener).run(); } 我们知道在ES里有主从分片的概念,所以一条数据被索引后需要经过两个阶段: 将数据写入Primary(主分片) 将数据写入Replication(从分片) 至于为什么不直接从Primary进行复制,而是将数据分别写入到Primary和Replication我觉得主要考虑如果一旦Primary是损坏的,不至于影响到Replication(考虑下,如果Primary是损坏的文件,然后所有的Replication如果是直接复制过来,就都坏了)。 又扯远了。我们看到doExecute 首先是进入PrimaryPhase阶段,也就是写主分片。 Primary Phase 在PrimaryPhase.doRun方法里,你会看到两行代码 final ShardIterator shardIt = shards(observer.observedState(), internalRequest); final ShardRouting primary = resolvePrimary(shardIt); 其中这个ShardIterator是类似 shardId->ShardGroup 的结构。不管这个shardId是什么,它一定是个Replication或者Primary的shardId, ShardGroup 就是Replication和Primary的集合。resolvePrimary方法则是遍历这个集合,然后找出Primary的过程。 知道Primary后就可以判断是转发到别的Node或者直接在本Node处理了: routeRequestOrPerformLocally(primary, shardIt); 如果Primary就在本节点,直接就处理了: //我去掉了一些无关代码哈 if (primary.currentNodeId().equals(observer.observedState().nodes().localNodeId())) { try { threadPool.executor(executor).execute(new AbstractRunnable() { @Override protected void doRun() throws Exception { performOnPrimary(primary, shardsIt); } } 这里用上了线程池。前面对每个shardId对应的数据集合做处理,其实是顺序循环执行的,这里实现了将数据处理异步化。 在performOnPrimary方法中,BulkShardRequest被转化成了PrimaryOperationRequest,理由也很简单,更加specific了,因为就是针对主分片的Request。接着进入shardOperationOnPrimary 方法,该方法是在孙子类TransportShardBulkAction类里实现的。 protected Tuple<BulkShardResponse, BulkShardRequest> shardOperationOnPrimary( ClusterState clusterState, PrimaryOperationRequest shardRequest) { 到该方法,有两个比较重要的概念会出现: //伟大的版本号,实现了对并发修改的支持 long[] preVersions = new long[request.items().length]; VersionType[] preVersionTypes = new VersionType[request.items().length]; //事物日志,为Shard Recovery以及 //避免过多的Index Commit做出突出贡献, //同时也是是实现了GetById的实时性 Translog.Location location = null; 上面两个概念成就了ES从一个简单的全文检索引擎到类No-SQL的转型(好吧,我好像又扯远了) 接着就是for循环了: //这里的request是BulkShardRequest //对应的items则是BulkItemRequest集合 for (int requestIndex = 0; requestIndex < request.items().length; requestIndex++) { 循环会根据BulkItemRequest的不同类型而有了分支。其实就是 IndexRequest,DeleteRequest,UpdateRequest,我们这里依然只讨论IndexRequest。如果发现BulkItemRequest是IndexRequest,进行如下操作: WriteResult<IndexResponse> result = shardIndexOperation(request, indexRequest, clusterState, indexShard, true); shardIndexOperation里嵌套的核心方法是executeIndexRequestOnPrimary,该方法第一步是获取到Operation对象, Engine.IndexingOperation operation = prepareIndexOperationOnPrimary(shardRequest, request, indexShard); Engine对象是比较底层的一个对象了,是对Lucene的IndexWriter,Searcher之类的封装。这里的Engine.IndexingOperation对应的是Create或者Index类。你可以把这两个类理解为待索引的Document,只是还带上了动作。 第二步是判断索引的Mapping是不是要动态更新,如果是,则更新。 第三步执行实际的建索引操作: final boolean created = operation.execute(indexShard); operation.execute 额外引出的话题 我们会暂时深入到operate.execute方法里,但这个不是主线,看完后记得回到上面那行代码上。 刚才我们说了operation可能是Create或者Index,我们会以Create为主线进行分析。所谓Create和Index,你可以理解为一个待索引的Document,只是带上动作的语义。 上面对应的execute 方法签名是: @Overridepublic boolean execute(IndexShard shard) { shard.create(this); return true; } 我们看到这里是反向调用indexShard对象的create方法来进行索引的创建。我们来看看IndexShard的create方法: //我依然做了删减,体现一些核心代码 public void create(Engine.Create create) { engine().create(create); } engine()方法返回的是InternalEngine实例,InternalEngine .innerCreate方法执行到构建索引的操作。这个方法值得分析一下,所以我就贴了一坨的代码。 private void innerCreate(Create create) throws IOException { if (engineConfig.isOptimizeAutoGenerateId() && create.autoGeneratedId() && !create.canHaveDuplicates()) { // We don't need to lock because this ID cannot be concurrently updated: innerCreateNoLock(create, Versions.NOT_FOUND, null); } else { synchronized (dirtyLock(create.uid())) { final long currentVersion; final VersionValue versionValue; versionValue = versionMap.getUnderLock(create.uid().bytes()); if (versionValue == null) { currentVersion = loadCurrentVersionFromIndex(create.uid()); } else { if (engineConfig.isEnableGcDeletes() && versionValue.delete() && (engineConfig.getThreadPool().estimatedTimeInMillis() - versionValue.time()) > engineConfig.getGcDeletesInMillis()) { currentVersion = Versions.NOT_FOUND; // deleted, and GC } else { currentVersion = versionValue.version(); } } innerCreateNoLock(create, currentVersion, versionValue); } } } 首先,如果满足如下三个条件就无需进行版本检查: index.optimize_auto_generated_id 被设置为true(默认是false,话说注释上说是默认是true,但是我看着觉得像是false) id设置为自动生成(没有人工设置id) create.canHaveDuplicates == false ,该参数一般是false 提这个是主要为了说明,譬如一般的运维日志啥的,就不要自己生成ID了,采用自动生成的ID,可以跳过版本检查,从而提高入库的效率。 第二个指的说的是,如果对应文档在缓存中没有找到(versionMap),那么就会由如下的代码执行实际磁盘查询操作: currentVersion = loadCurrentVersionFromIndex(create.uid()); 通过对比create对象里的版本号和从索引文件里加载的版本号 ,最终决定是进行update还是create操作。 在innerCreateNoLock 方法里,你会看到熟悉的Lucene操作,譬如: indexWriter.addDocument(index.docs().get(0)); //或者 indexWriter.updateDocument(index.uid(), index.docs().get(0)); 现在回到TransportShardBulkAction的主线上。执行完下面的代码后: final boolean created = operation.execute(indexShard); 就能获得对应文档的版本等信息,这些信息会更新对应的IndexRequest等对象。 到目前为止,Primay Phase 完成,接着开始Replication Phase replicationPhase = new ReplicationPhase(shardsIt, primaryResponse.v2(), primaryResponse.v1(), observer, primary, internalRequest, listener, indexShardReference); finishAndMoveToReplication(replicationPhase); 最后一行代码会启动replicationPhase阶段。 Replication Phase Replication Phase 流程大致和Primary Phase 相同,就不做过详细的解决,我这里简单提及一下。 ReplicationPhase的doRun方法是入口,核心方法是performOnReplica,如果发现Replication shardId所属的节点就是自己的话,异步执行shardOperationOnReplica,大体逻辑如下: threadPool.executor(executor).execute(new AbstractRunnable() { @Override protected void doRun() { try { shardOperationOnReplica(shard.shardId(), replicaRequest); onReplicaSuccess(); } catch (Throwable e) { onReplicaFailure(nodeId, e); failReplicaIfNeeded(shard.index(), shard.id(), e); } } 在Replication阶段,shardOperationOnReplica 该方法完成了索引内容解析,mapping动态新增,最后进入索引(和就是前面提到的operation.execute)等动作,所以还是比Primary 阶段更紧凑些。 另外,在Primary Phase 和 Replication Phase, 一个BulkShardRequest 处理完成后(也就是一个Shard 对应的数据集合)才会刷写Translog日志。所以如果发生数据丢失,则可能是多条数据。 总结 这篇文章以流程分析为主,很多细节我们依然没有讲解详细,比如Translog和Version。这些争取能够在后续文章中进一步阐述。另外错误之处在所难免,请大家在评论处提出。

优秀的个人博客,低调大师

Android短视频开发业务中视频编解码的相关知识阅读

随着互联网对人们生活习惯的改变,Android短视频开发业务不断升温。移动端各个媒体平台成为流量市场中增长的主力,各式各样的短视频应用迅速抢占人们的手机屏幕。而在Android端想要实现录制功能,就需要储备相关的知识,以实现720P、30帧为例选择一个正确的实现方案,避免CPU的消耗太大造成手机发烫的现象。比如今天想跟大家分享的关于视频的编解码的相关知识。 软件编码和硬件编码定义 1.软件编码即采用CPU对相机所采集的原始数据进行编码,然后跟音频一起合并成为MP4格式的文件。这一方案的优点是, 2.硬件编码即采用手机提供的硬编接口,利用其中的硬件芯片直接进行编码的合成。 区别 1.ÂÂÂÂ 软件编码相对来说技术比较成熟,实现起来快而且兼容性也好。缺点就是,软件编码的CPU占用率高,像一些性能比较差的手机可能就无法达到我们所说的720p,30帧。而且还会同时引用大量的第三方库,导致包体过大。 2.ÂÂÂÂ 而硬件编码的速度非常快且效率极高,CPU的占用率低,简单点讲就是即使我们长时间的保持高清录制,手机也不会发烫。缺点是,由于某些智能手机的机型问题,需要处理兼容性方面的问题,同时Android端上的硬件编码和Surface以及openGL的关系相对也比较密切。对于这一方的知识,还需要多查阅多摸索才行。 软件解码和硬件解码 1.硬件解码就是通过硬件进行视频的解码工作,其中硬件解码是由GPU来进行的,使用GPU解码能够降低CPU的工作负荷从而降低功耗。 2.软件解码则是通过软件本身占用的CPU进行解码,所以会增加CPU工作负荷造成功耗上升。 区别 1.硬件解码播放出来的视频更加流畅,而且能够延长移动设备终端播放视频的时间。而软件解码由于会加大CPU工作负荷,导致占用了过多的移动CPU资源,如果手机的CPU能力不足,那么软件也会受到一定影响。 2.硬件解码所需要的解码视频格式容易受到GPU的影响,导致无法播放全部视频,画质也不够清晰。而软件解码能解码所有的视频格式文件,且画质更加清晰。 综合以上内容我们可以看出,在Android短视频开发业务中,硬件编解码方式比较合适。因为短视频的本地录制并不像直播,对带宽、实时性和互动性等方面要求严格,比如编码需要进行动态调节编码器中码率帧率的情况,所以本地录制可以将编码器的码率设置的比较高一些,也不需要进行动态的调节来改变视频分辨率。

优秀的个人博客,低调大师

第一行代码阅读笔记----显示隐式Intent的基本用法

1、显示Intent意图明显,通过Intent启动另外一个活动,这是安卓中各组件进行交互的一种重要方式。一般用于启动活动,启动服务,发送广播等场景。 实现方法,这里我只说思路,实践还是要自己实操才能明白: 1、首先多创建一个activity,命名为别的,这个就是待会咱们要调用的另外一个activity,可以跟主activity一样。 2、创建完后记得在AndroidManifest.xml中为新创建的activity进行注册。 同样的,要将其包括在<application分支下 ep: <activity android:name=".SecondActivity" > </activity> 3、由于我们创建的这一个新的活动并不是主活动,所以不需要配置<intent-filter>标签里的内容,所以不用理会 4、使用Intent实现调用另外一个activity Intent有多个构造函数的重载,其中一个是 Intent(Context packageContext, Class<?> cls)。 这个构造函数接收两个参数,第一个参数 Context 要求提供一个启动活动的上下文,第二个 参数 Class 则是指定想要启动的目标活动,通过这个构造函数就可以构建出 Intent 的“意图”。 所以,称做显示Intent ep: button1.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { //我在按键单击事件中实现这个过程 //当我按下button的时候,此时FirstActivity就会去调用SecondActivity,跳到另外一个界面去 //返回主界面也很简单,只要按下手机上的back键就可以了 Intent intent = new Intent(FirstActivity.this, SecondActivity.class); //启动活动 startActivity(intent); } });2、隐式Intent 按照作者的书籍可以理解为,隐式就没有那么高调的去启动,而是偷偷摸摸的,比较低调,含蓄的作风。 它并不明确的想要去启动哪一个活动,而是通过更为抽象的action和category等信息,交由系统去分析这个 Intent,并去找出合适的活动去启动。 那么如何隐式启动第二个活动呢? 1、还是一样,和上面显式的一样第一个步骤 2、打开AndroidManifest.xml配置<intent-filter>标签里的内容 <activity android:name=".SecondActivity" > <intent-filter> //指明了当前活动能够相应的Intent中带有的category //这里要求只有action和category中的内容同时能够匹配上Intent中指定的action和category时,这个活动才能响应Intent <action android:name="com.example.activitytest.ACTION_START" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity> 3、在事件中添加Intent ep: button1.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent("com.example.activitytest.ACTION_START"); startActivity(intent); } });

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册