<![CDATA[阿琛]]>https://noahzy.com/https://noahzy.com/favicon.png<![CDATA[阿琛]]>https://noahzy.com/Ghost 2.25Mon, 04 Dec 2023 15:30:36 GMT60<![CDATA[阿琛]]>互联网金融行业发生了翻天覆地的变化,相对应的金融科技也在不断的更新和迭代,每次有新的软件系统出炉的时候,就是老的软件系统命运终结的开始,老的项目当然不会束手就擒,它也会做最后的挣扎,当你从它身上迁移用户或者商户的时候,它会给你带来很多麻烦,比如说,它会临时罢工、出现资金损失等等不可忽视的问题,因此,迁移是个大任务,有的时候迁移并不亚于开发一套新系统的难度,甚至可以说是有过之而无不及。

哪些场景需要迁移

我们总结了各种需要迁移的场景。

字段迁移

原来设计的字段大小不能满足现在业务的需求,直接在原表上扩容字段可能会影响线上跑的业务,因此,我们需要增加一个字段来替换原来的字段;字段的数据格式需要升级,通过新增字段来替换原有字段,例如:原有未加密的字段处于安全需求进行加密。

表迁移

用于数据库表设计重构的场景,原有表的结构不合理,新增合理的表来替换原有的表,这时候我们需要表迁移。

数据库迁移

把单库迁移到分库分表的多个库;从一种数据库迁移到另外一种数据库;分库分表的多个库需要扩容的时候,需要进行数据库迁移,迁移到一套能够容纳足够数据的数据库集群。

数据库迁移到其他类型的库

对数据库的选型发生了变化,例如:微博的粉丝库从原有的 MySQL 迁移到 HBase。

系统迁移

原有系统的技术已经过时了,不能满足新需求或者业务发展的需要,开发完成新系统来替换原有系统。

通用的迁移方法论

通用的迁移方法分为平滑迁移和停机迁移,

]]>
https://noahzy.com/tong-yong-jia-gou-shi-ying-gai-ru-he-ba-kong-qian-yi-ji-zhu-fang-an/62f1ca6d34a5e44df46594baTue, 09 Aug 2022 06:43:24 GMT互联网金融行业发生了翻天覆地的变化,相对应的金融科技也在不断的更新和迭代,每次有新的软件系统出炉的时候,就是老的软件系统命运终结的开始,老的项目当然不会束手就擒,它也会做最后的挣扎,当你从它身上迁移用户或者商户的时候,它会给你带来很多麻烦,比如说,它会临时罢工、出现资金损失等等不可忽视的问题,因此,迁移是个大任务,有的时候迁移并不亚于开发一套新系统的难度,甚至可以说是有过之而无不及。

哪些场景需要迁移

我们总结了各种需要迁移的场景。

字段迁移

原来设计的字段大小不能满足现在业务的需求,直接在原表上扩容字段可能会影响线上跑的业务,因此,我们需要增加一个字段来替换原来的字段;字段的数据格式需要升级,通过新增字段来替换原有字段,例如:原有未加密的字段处于安全需求进行加密。

表迁移

用于数据库表设计重构的场景,原有表的结构不合理,新增合理的表来替换原有的表,这时候我们需要表迁移。

数据库迁移

把单库迁移到分库分表的多个库;从一种数据库迁移到另外一种数据库;分库分表的多个库需要扩容的时候,需要进行数据库迁移,迁移到一套能够容纳足够数据的数据库集群。

数据库迁移到其他类型的库

对数据库的选型发生了变化,例如:微博的粉丝库从原有的 MySQL 迁移到 HBase。

系统迁移

原有系统的技术已经过时了,不能满足新需求或者业务发展的需要,开发完成新系统来替换原有系统。

通用的迁移方法论

通用的迁移方法分为平滑迁移和停机迁移,平滑迁移一般采用双写的方案,不需要停机就可以完成迁移,类似飞机的空中加油,或者给运行的汽车换轮子。而停机迁移,则需要停止现有的业务服务,然后迁移数据,待数据迁移之后,再开启对外提供服务。

这里讲解的方法论是一种通用的迁移方案,既适合字段迁移、表迁移,也适合库迁移以及应用迁移,既适合数据库的迁移也适合类似雷竞技app的迁移,虽然在细节上有些不同,但是在方法论上则大同小异,我们以分片后的数据容量不能满足需求,需要对分片后的数据扩容为例,这里的扩容实际上是迁移的一种特殊案例,我们以扩容为例来说明相应的步骤和实现细节。

平滑迁移

平滑迁移适合对可用性要求较高的场景,例如,线上的交易服务对类似雷竞技app或者数据库依赖较大,不能忍受停机带来的业务损失,也没有交易的低峰期,我们对此只能采用平滑迁移的方式。

平滑迁移,是指将正在提供线上服务的数据,从一个地方数据存储到另一个数据存储,整个迁移过程中要求不停机,服务不受影响。根据数据所处层次,可以分为类似雷竞技app迁移、数据库存储迁移、应用迁移等等;根据数据迁移前后的变化,又可以分为数据平移和数据转移。数据库对数据库的迁移属于平移,数据库对其他 NoSQL 库的迁移属于数据转移。

数据平移是指迁移前后数据组织形式不变,比如 MySQL 数据库从1个实例扩展为4个实例,Redis 类似雷竞技app从2个实例扩展到8个实例等等。

如果在最初的设计里就为以后的扩容做好准备,也就是做了充分的容量评估(关于容量评估,请参考《分布式服务架构:原理、设计与实战》一书中第3章的内容),那么数据迁移工作就会简单很多,比如 MySQL 已经做了分库分表,扩展实例的时候,只需要多做几个从库,切换访问关系,最后将冗余的库表删除即可达到扩容的效果,当然,这需要短暂的停止服务。

近年来出现很多支持自动可伸缩的数据库,在实现上已经做到全自动数据迁移,如 HBase、TiDB 等,那就更简单了,只要通过管理功能来添加机器,手工修改配置或者系统自动发现,就可完成数据库容,也就免去了复杂的数据迁移等工作。

数据转移是指在数据迁移前后,数据组织形式发生了变化。比如将 MySQL 数据库迁移到 HBase 数据库,微博就经历过这样的过程。

平滑迁移通常使用的是双写方案,方案分成4个步骤:双写、迁移历史数据、切读、下双写。

这种方式如果应用于类似雷竞技app扩容的迁移场景,则还有一个变种,就是不需要迁移旧数据,在第1步中双写后,在一定的时间里通过新规则对新类似雷竞技app进行写入,新类似雷竞技app已经有了足够的数据,这样我们就不用再迁移旧数据,直接进入第3步即可。

首先,假设我们的应用现在使用了具有两个分片的数据集群,通过关键字哈希的方式进行路由,如下图所示。

因为两个分片已经不能满足容量的需求,所以现在需要扩容到4个分片,达到原来两倍的总大小,因此我们需要迁移。

迁移的具体过程如下。

(1)双写

按照新规则和旧规则同时往新旧数据系统中写数据,如下图所示。

这里,我们仍然按照旧的规则,也就是关键字哈希除以2取余来路由分片,同时按照新的规则,也就是关键字哈希除以4取余来路由到新的4个分片上,来完成数据的双写。

这个步骤有优化的空间,因为是在成倍扩容,其实,我们不需要准备4个全新的分片,对于新规则中的前两个分片的数据,其实是旧规则中的两个分片数据的子集,并且规则一致,所以我们可以重用前两个分片,也就是一共需要两个新的分片,用来处理关键字哈希取余后为2和3的情况,使用旧的数据分片来处理关键字哈希取余后0和1的情况即可。如下图所示。

(2)迁移历史数据

把旧类似雷竞技app集群中的历史数据读取出来,按照新的规则写到新的数据集群中,如下图所示。

在这个过程中,我们需要迁移历史数据,在迁移的过程中可能需要迁移工具,这也需要一部分开发工作量。在迁移后,我们还需要对迁移的数据进行验证,表明我们的数据迁移成功。

在某些应用场景下,例如类似雷竞技app数据,并不是应用强依赖的,在类似雷竞技app里获取不到数据,可以回源到数据库获取,因此在这种场景下通过容量评估,数据库可以承受回源导致的压力增加,就可以避免迁移旧数据。

在另一种场景下,数据一般是具有时效性的,应用在双写期间不断向新的集群中写入新数据,历史数据会逐渐过时,并被从旧的集群中删除,在一定的时间流逝后,新的集群中自然就有了最新的数据,就不再需要迁移历史数据了,但是这需要进行评估和验证。

(3)切读

把应用层所有的读操作路由到新的数据集群上,如下图所示。

在这一步骤里,把应用中读取的操作的数据源转换成新的数据集群,这时应用的读写操作已经完全发生在新的数据库集群上了。这一步一般不需要上线代码,我们会在一开始上双写时就实现开关逻辑,这里只需要将读的开关切换到新的集群即可。

(4)下线双写

在这一步,我们把写入旧的集群的逻辑下线,如下图所示。

这一步通常是在双写和切读后验证没有任何问题,并保证数据一致性的情况下,才把这部分代码下线。同时可以把旧的分片下线,如果是扩容的场景,并且重用了旧的分片1和分片2,则还可以清理分片1和分片2中的冗余数据。

对于字段迁移来讲,我们除了新增字段,双写后替换原来字段,我们还可以采用原地替换的方法,对于新数据加密,加密后做标志,然后异步的将历史数据加密,让查询程序兼容加密的数据和非加密的数据。

可以用“软着陆”来形容双写迁移方案,这和新领导上任后,一般先招心腹,慢慢的替代老下属的职责,慢慢淘汰老下属,慢慢实现软着陆如出一辙。

停机迁移

停机迁移的方法比较简单,通常分为停止应用、迁移旧数据、更改数据源文件、启动应用这4个步骤,如下图所示。

具体的迁移步骤如下。

  1. 停机应用,先将应用停止服务。
  2. 迁移历史数据,按照新的规则把历史数据迁移到新的类似雷竞技app集群中。
  3. 更改应用配置,指向新的类似雷竞技app集群。
  4. 重新启动应用。

这种方式的好处是实现比较简单、高效,能够有效避免数据的不一致,但是需要由业务方评估影响,一般在晚上交易量比较小或者非核心服务的场景下比较适用。

如何验证迁移成功

迁移过程中最重要的一环就是如何验证迁移方案是成功的,我曾经给小伙伴们定了一个顺口溜。

要明确什么样角色的什么人在什么时候到什么系统看到什么样的数据,才能确认迁移成功。

虽然这是看似简单的一句话,但是信息量满满的,有了这句话作为指导,能够确保迁移方案的方向一定是正确的,一定不会导致太大的问题。

这句话具体包括几个要素:谁、什么时候、什么样的数据,这些必须在迁移方案中都要事先明确,具体执行的人必须了解清楚这些条款,假设我们在迁移支付行业中的计费模板,那么迁移切量后,我们就需要计费的运营在切量后的第一时间到计费系统中查看计费的准确性,并且明确什么样的计费结果是准确的,什么样的计费结果是不准确的。

有的时候只靠人来确定数据是否正确恐怕还不够,我们通常需要工具自动化的进行比对,同样我们以计费模板迁移为例,我们从一套计费模板迁移到另外一套计费模板中,通常在初期我们不会真正的以新模板为准,我们会在程序中实现“双计”,既使用老的计费模板计费,也使用新的计费模板计费,在初期我们以老的计费模板为准,新的计费模板计费的结果只用于比对,并且计入日志,这样通过程序自动比对一段时间以后,发现新的计费模板确实没有问题,我们才真的开启开关,以新的计费模板为准。

当然,有的时候数据量巨大,我们比对每一个流量产生的数据不太现实,也会严重影响性能,这个时候我们需要对数据比对进行抽样,只对一些比较有代表性的数据进行比对。

系统迁移后数据清洗

系统迁移过程中,上了双写以后,历史数据仍然保留在老系统,因此,我们需要将历史数据迁移到新系统,因为,有些时候我们需要读取或者修改历史数据,例如支付行业的退款等。

我们把迁移历史数据的过程称为洗数据,通常使用如下的步骤来实现。

  1. 先清洗历史数据,将清洗后的数据写入新系统。
  2. 做全量对比,如果数据太多,没法全量对比,就抽样对比,看看有没有不一致的数据。
  3. 在数据量巨大的情况下,线上系统复杂,出现少量的不一致是正常的,这时候不一致的数据进行分析,这时候可能需要参考线上服务系统的交易日志,查明造成不一致的原因,并进行修复。

这里,有读者会对上面第2步有疑问,为什么会产生不一致的数据呢,这有很多原因。下面我们来仔细分析。

对于增加记录,到迁移历史数据这一阶段,我们使用的是双写,因此,数据在新老数据库中都会存在,即使双写有问题,导致一方不存在,我们也可以通过比对来补齐。

对于更新数据,则容易产生不一致,导致新老数据库的数据不一致,假设在迁移某条历史数据的过程中,线上的交易系统正好修改了这条数据,在双写系统还没有更新历史数据的时候,迁移工具已经把这条数据拿到了应用系统中,这时数据在新老库中都更新了,但是迁移工具后续又把老版本的这条数据更新到新系统中,就导致数据不一致。

对于删除数据,和上面更新数据有一样的问题,也会导致不一致的问题,这时候以谁为准,怎么保证一致性是个难题,我们需要借助修改的 timestamp 或者版本来区分哪一个是最新的版本,然后对数据进行修复。还有另外一个更好的方法,对于某些业务,数据是有一定时效性的,超过一定时间的数据就不再更改,因此,我们可以让双写的时间拉长,要长于数据更新的时间段,这样在历史数据完全不被更新的时候,我们再进行洗数据,就不会因为迁移而产生不一致了。

迁移失败怎么办

这里探讨,迁移失败了,迁移后验证迁移有问题了,怎么办?其实,迁移失败了没有什么好的办法,只有两个途径可以走,一个就是迁移失败了就在新系统中修复,但是有些时候这可能会导致更多不可用时间,另外一个方案就是迁移失败了,需要迁移回来,但是迁移回来迁移过程中产生的数据怎么办?

这是唯一能用的两个办法,没有更好的办法能解决迁移失败的问题,因此,在这个问题上我们不能奢求完美的解决方案,我们只能退而求其次,提前做好应对方案。

如果我们决定了迁移失败了就在新系统中修复,而不再切回老的系统,我们就要充分的做好应急方案,一旦这种事情出现了,我们要预测可能产生的问题,针对问题做相应的解决方案,甚至我们提前做好工具,在问题出现的时候,我们要快速发现和快速恢复。

如果我们决定了迁移失败后要迁移回来老系统,我们也要提前做好应急方案,应急方案中要包含如何发现问题,如何迁移回老系统,将流量迁移回老系统之后,还要考虑在新系统中遗留的数据怎么办,通常来讲,我们有两个方法,一种方法是通过工具把这些数据迁移回老系统,另外一种方法是让老系统兼容新数据。

迁移方案的评审

这里我们详细阐述作为架构师应该如何评审迁移方案。

首先,需要有人牵头写即将要评审的迁移方案,迁移方案的内容要包括具体的迁移产品,迁移的目的是什么,描述为什么要进行此次迁移,以及要达到什么效果,迁移任务的时间点,迁移方案什么时候在系统上实施,什么时候真正的进行切量,什么时候进行验证,什么时候结束,迁移过程中有什么原则,迁移会影响多少用户和商户,影响到什么程度,这次迁移的主要负责人是谁,参与人是谁,这些都需要落实到纸质的文档。

然后,我们最需要考虑的就是这次迁移的影响范围,都对哪些角色的人会产生影响,以及对哪些人是透明的,这要评估是否对商户、用户、运营人员等有感知,如果对任何人有感知,需要制定提前通知的方案,要通知哪些人,什么时候通知,以什么形式通知,是否需要被通知人回复认可等。

接下来需要确认迁移用户和商户的选择、顺序、批次,一般选择不重要的用户和商户先迁移,验证迁移过程没问题,再迁移重要的用户和商户,要综合考虑用户和商户的等级、交易类型、迁移复杂度等,以此确定用户和商户迁移顺序和批次。

通常,在全量切换之前,我们需要进行多次验证,我们需要在准生产测试迁移变更逻辑和开关逻辑,或者通过 TCPCopy 环境来验证代码变更的正确性,然后,通过少量的内部用户在线上验证逻辑的正确性,最后,会按照我们选定的用户和商户的批量,逐渐的放量、观察和验证,最后再全量切换。

然后,要确定迁移过程中对系统的变更内容,要确定变更的关键内容,然后做测试方案,要测试到所有的场景,包括迁移开关的开和关,要测试迁移失败后迁移回老系统的情况,不要抱着侥幸态度就忽略这部分的测试。

最后,进入迁移方案的关键内容,要对迁移的过程识别风险,这包括交易风险、业务风险、系统风险、技术风险和政策风向等,要对迁移过程中更改的信息流和资金流进行详细评估,对识别的风险要给出应对方案。

迁移开关和迁移工具

在迁移的过程中,除了要配合迁移开发系统,还有两个比较特殊的工作。

一个就是迁移开关的设计,在迁移的过程中,双写、切流量、有问题了切回流量,这些都需要使用迁移开关,迁移开关的设计非常的重要,如果迁移开关设计的不合理会产生很大问题,甚至会导致资金损失,在我经历过的金融系统中,曾经经历过迁移开关设计在统一的共享类似雷竞技app上,由于网络原因重试导致请求流量重复,请求流量走到了不同的应用节点上,不同应用节点读取共享类似雷竞技app有时差,导致两个流量一个走了开的逻辑,一个走了关的逻辑,如果后端系统没有做幂等,这会导致资金损失。因此,我们对迁移开关的设计,制定了如下的最佳实践。

  1. 迁移开关要做在订单上,在订单上标记是否迁移新系统。
  2. 迁移开关要有不同的维度,可在订单上,可在商户或者用户上,可在系统级别。
  3. 迁移开关要能开能关,也就是流量要能切到新系统,也要能切到老系统。

另外一个重要的任务是开发迁移工具,比如说迁移后校验数据,对比数据等,这都需要开发专业的工具,靠运营对比大量的数据很容易产生误差,如果想做好迁移,就要舍得成本来投入到这些关键任务上。

迁移过程中的政治因素

迁移是个费力不讨好的工作,因此,很多人其实不愿意干这个活,迁移做得好,那是应该做的事情,迁移出现了问题,那全是工作没做好,况且迁移总不会顺顺利利的,这就是为什么大家不愿意做,越是有挑战的任务,其实,它的内在价值就越大。

但是要真的想做一次彻底的迁移,替换掉老的系统,这需要一定的激励措施,要与迁移负责人和参与人明确迁移的目标和价值,一旦按照计划迁移成功除了迁移过程中给大家带来的经验,还有什么样的奖励,否则,只是作为一个常规任务,那么参与者必然失去兴趣,迁移也就成了挠痒痒,基本就变成今天迁移一点流量,明天迁移一点流量,最后就新老系统并存,不伦不类的。

迁移一定要由架构组来把关

数据迁移并不是一项需要高大上的技术工作,它需要的是对业务逻辑的把控,对操作流程的理解,对新旧系统特性和环境的掌握,以及对细节的掌控,要深入骨髓般的理解系统才能做好,因此迁移是需要架构组来把关的。

但是,迁移的事情也不是那么简单的,也不能由运营单独搞定的,这涉及到迁移工具的开发,迁移后的验证,迁移失败如何迁回,脏数据如何处理,迁移过程中如何平滑过度,例如迁移计费模板的过程,应该开发工具进行对比结果后,才能真正的使用新的计费系统,这些都是必须有业务人员和技术人员来共同完成的,因此,迁移工作最好由架构组牵头,由产品、运营、技术等一起来实施。

如何汇报迁移技术方案

由于迁移是个非常大的任务,设计的部门和角色比较多,由于出了问题产生的影响比较大,因此,很多老板都会关注迁移的方案,这里,对于迁移负责人和架构师,要负责给不同角色的人汇报迁移方案,这里我总结的一个最佳实践是不同角色的人关注不同的内容。

  1. 销售的老板会关注迁移后带来的价值,带来哪些系统能力的提升,能够带来多少毛利的增长。
  2. 业务的老板关注的是业务的风险,关注迁移以后是否带来产品能力的提升等等。也会关注成本的降低;还会关注迁移的里程碑,时间点等。
  3. 运营的老板更会关注迁移的实施方案,包括如何通知商户、如何选择商户等。
  4. 而技术的老板会关注具体的迁移设计方案本身。

因此,在给不同的老板做汇报的时候,我们汇报内容的侧重点要有所不同。

[a](https://blog.csdn.net/qq_16605855/article/details/80966363)

]]>
<![CDATA[阿琛]]>本文转载自:https://www.jianshu.com/p/f8edd7b7a217

本文主要涉及iOS的国际化,网上虽然有很多相关的文章,但是仔细阅读下来感觉都不太全面,因此重开一篇总结,记录项目中遇到的所有要点,demo见最下方链接。

1. App名称国际化

2. 图片、文字国际化

3. 强制默认显示某种语言

4. 启动图国际化

5. iOS10所需的权限配置国际化

6. xib/storyboard国际化

7. 总结


1.App名称国际化

非常简单地按步骤修改就可以了。

PROJECT-Info-Localizations中点击下方的小“+”,添加需要添加的语言,本文中以简体中文和英文为例。(国际化的所有操作,都需要这一步作为前提。)


添加以InfoPlist.string为名称的string文件。查到的资料都说需要名称一模一样才能使用,没试过其他的名字。


选中新建好的InfoPlist.string,点击Localize按钮,添加语言。


完成上一步骤后在右边勾选所需要语言,Xcode会自动创建对应的string文件。


分别在对应的string文件中填写App名称就可以了。


*关于Bundle name和Bundle

]]>
https://noahzy.com/ioskai-fa-ji-qiao-guo-ji-hua-localization-zhi-kan-yi-pian-jiu-gou-liao/5d7762837fa7c611a5d555d0Tue, 10 Sep 2019 08:56:36 GMT本文转载自:https://www.jianshu.com/p/f8edd7b7a217

本文主要涉及iOS的国际化,网上虽然有很多相关的文章,但是仔细阅读下来感觉都不太全面,因此重开一篇总结,记录项目中遇到的所有要点,demo见最下方链接。

1. App名称国际化

2. 图片、文字国际化

3. 强制默认显示某种语言

4. 启动图国际化

5. iOS10所需的权限配置国际化

6. xib/storyboard国际化

7. 总结


1.App名称国际化

非常简单地按步骤修改就可以了。

PROJECT-Info-Localizations中点击下方的小“+”,添加需要添加的语言,本文中以简体中文和英文为例。(国际化的所有操作,都需要这一步作为前提。)


添加以InfoPlist.string为名称的string文件。查到的资料都说需要名称一模一样才能使用,没试过其他的名字。


选中新建好的InfoPlist.string,点击Localize按钮,添加语言。


完成上一步骤后在右边勾选所需要语言,Xcode会自动创建对应的string文件。


分别在对应的string文件中填写App名称就可以了。


*关于Bundle name和Bundle display name:

stackoverflow.com/questions/9667582/bundle-name-and-bundle-display-name

就显示来说,Bundle display name关系着icon下方的App名称文本显示,而Bundle name则作为存储App的文件夹名称,并没有什么影响。


*用以上方法修改App名称的一个问题:

如果系统为有对应string文件的语言时,可以正常显示。

如果系统为无对应string文件的语言时,删除App重装后会跟随设定的开发语言显示;直接修改系统语言时会跟随上一次有对应string文件时的语言显示。

设定开发语言


2.图片、文字

仍是非常简单地按照步骤修改即可。

首先创建一个string文件,名称为Localizable.string。


选中Localizable.string,点击右边的Localize按钮,在弹框的下拉菜单中随便选一个需要添加string文件的语言,确认。(操作同InfoPlist.string的)


右边的小勾要点上,勾选了之后Xcode才会自动创建对应的string文件。


在对应的语言的Localizable.string文件中添加对应的图片名称和文本内容。

"mainImage" = "mainImage_cn";//等号左边为代码需要调用的key,右边为对应的中文图片名称value。

"mainText" = "Chinese";//等号左边为代码中需要调用的key,右边为对应的中文文本value。


最后,只要在代码中需要显示图片和文字的部分使用Foundation框架中的NSLocalizedString(key, comment)调用即可。

// 程序将根据第一个参数去对应语言的文件中取对应的值,第二个参数将转化为字符串文件里的注释,可以传nil,也可以传空字符串@""。

//#defineNSLocalizedString(key,comment) [[NSBundle mainBundle] localizedStringForKey:(key)value:@""table:nil]

//图片调用

UIImageView *imageView = [[UIImageView alloc]initWithImage:[UIImage imageNamed:NSLocalizedString(@"mainImage", nil) ]];

//文本调用

textLabel.text = NSLocalizedString(@"mainText", nil);

修改语言后的显示:


*以上的操作后,代码中文字和图片的国际化已经完成了,但是在更换系统语言后会遇到一些问题。

我的项目中只做了简体中文和英文的语言设置,如果系统语言为日文时安装App,App中语言会根据我设置的开发语言显示为英文。如果在App已安装后更改系统语言为日文,则会跟随App上一次的语言设置作相同的显示。这样并不符合我的项目需求,因此我加入了下方的第三部分。



3.强制默认显示某种语言

我的项目需求:在系统语言选定为中文时,App语言为中文,其他情况下全部作英文显示。

因此需要添加系统语言读取,经过判断后直接调用我需要的某种语言对应的值来显示。

//在Appdelegate.m中添加系统语言检测与赋值

NSArray *languages = [NSLocale preferredLanguages];

NSString *language = [languages objectAtIndex:0];

if ([language hasPrefix:@"zh"]) {//检测开头匹配,是否为中文

[[NSUserDefaults standardUserDefaults] setObject:@"zh-Hans" forKey:@"appLanguage"];//App语言设置为中文

}else{//其他语言

[[NSUserDefaults standardUserDefaults] setObject:@"en" forKey:@"appLanguage"];//App语言设置为英文

}

//在需要的部分添加手动选取语言的宏,并调用得到对应的值

//宏

#define Localized(key)  [[NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:@"%@",[[NSUserDefaults standardUserDefaults] objectForKey:@"appLanguage"]] ofType:@"lproj"]] localizedStringForKey:(key) value:nil table:@"Localizable"]

//调用

forceLabel.text = Localized(@"forceText");


选定系统语言为中文,安装App并运行(显示结果为图1),之后修改系统语言为日文(显示结果为图2),使用NSLocalizedString国际化的文字显示为中文,使用自定义宏Localized国际化的文字显示为英文。

图1

图2



4.启动图

启动图国际化的方法有两种,一种是在Info.plist中添加图片(需要导入的图片较多),一种是添加不同的storyboard分别调用(需要导入的图片少些)。操作很简单,但是我在实际使用中遇到了两个问题花费了一些时间,问题将附于下方做一些简述。



4.1 图片+Info.plist使启动图国际化

这个方法同样可以用作App中图片的国际化,但是我比较习惯把图片导入Assets.xcassets中使用,因此在图片国际化中没有做介绍。这个方法主要是在Info.plist中直接配置启动页的参数来展示启动图,下方附上官方的文档。

developer.apple.com/library/content/documentation/General/Reference/InfoPlistKeyReference/Articles/iPhoneOSKeys.html

首先取消默认的启动图,将TARGET-General-Launch Screen File中默认的LaunchScreen去掉,留空。


导入准备好的启动图,这里我做了3.5、4、4.7、5.5英寸的。之后选中图片,点击Localize按钮,这个步骤和string文件的操作一样,四个图片都要做。


右键点击设置好的片,选中Show in Finder。下图中,en.lproj中保存的是英文版的启动图,zh-Hans.lproj保存的是中文的启动图。把准备好的对应的图片修改名称后拖入就行了,图片名称一定要和工程中导入的一样。


图片准备完毕之后,将Info.plist以源码的形式打开,或者是直接在属性列表中点击“+”添加对应的属性。


Source Code中对应的代码:(demo的Sourcecode Codes中有)


Property List中对应的列表:


删除原先安装的App,重新安装后即可看到效果。


运行后如果看不到启动图更换效果,请删除App后重新运行。



4.2 storyboard+InfoPlist.string使启动图国际化

首先创建作为启动页的storyboard,分别命名区分。我设置的名字是LaunchImage_En和LaunchImage_Ch,名字自己分得清就可以啦。


storyboard中添加ViewController,勾选Is Initial View Controller(中英文的启动页都需要勾选),添加启动图,添加约束占满屏。


设置好后在Info.plist中添加启动图名称。值我填了项目默认的LaunchScreen,但是并不用它作为启动图,后面还要手动赋值。


右键点击Show Raw Keys/Values看一下key。并在InfoPlist.string中手动将一开始创建的两个中英文启动图的storyboard值赋进。

storyboard更改启动图国际化完成。


下面是一开始做国际化时遇到的关于启动图的两个问题。

*想要看到启动图的效果,必须删除原有App后重新安装。查找了一些相关的资料后发现,启动图的资源只会保留一份,在已有的情况下不会重新生成,根据苹果的用户交互指引,该页面是在程序加载时显示的,不建议动态修改。在比较了一些其他的App,例如QQ、DJI GO、淘宝、微博等,有些启动图使用的是可以在不同语言系统下共用的图片,其他的也并没有做动态修改。

*使用storyboard修改启动图时一定要记得在Info.plist中添加Launch screen interface file base name,不然无法显示启动图,运行后会看到App的icon变成了启动图,App的视图大小也会有问题。



5. iOS10所需的权限配置

同storyboard设置启动图国际化。

首先在Info.plist中添加好权限后右键,选择Show Raw Keys/Values看一下key。


把key复制进InfoPlist.string中分别写好对应的中英文描述即可。



6. xib/storyboard国际化

官方文档中有图有真相地描述过啦!

developer.apple.com/library/content/documentation/MacOSX/Conceptual/BPInternational/LocalizingYourApp/LocalizingYourApp.html



7. 总结

iOS的项目国际化中并没有什么难点,主要是找到的一些描述比较含糊或者不全。

国际化其实就是为各个语言单独创建一份资源,通过名称为*.lproj文件夹来保存。在勾选需要的语言后,Xcode会自动创建对应的文件,修改其中的值即可。

demo地址:github.com/Linciay/TYLocalization

]]>
<![CDATA[阿琛]]>最近自己搭建了一个GitLab CE服务器,一些命令行需要记下来


[官方配置文档](http://docs.gitlab.com/ce/)

配置文件路径

/etc/gitlab/gitlab.rb

配置文件修改之后要执行下面语句才能生效

sudo gitlab-ctl reconfigure

启动gitlab所有组件

sudo gitlab-ctl start

停止gitlab所有组件

sudo gitlab-ctl stop

重启gitab所有组件

sudo gitlab-ctl restart

PostgreSQL

http://docs.gitlab.com/omnibus/settings/database.html
进入db
sudo gitlab-rails dbconsole

GitLab恢复备份说明

]]>
https://noahzy.com/gitlab-yi-xie-yao-ji-zhu-de-ming-ling/5d7761db7fa7c611a5d555bfTue, 10 Sep 2019 08:43:24 GMT最近自己搭建了一个GitLab CE服务器,一些命令行需要记下来【GitLab】一些要记住的命令


[官方配置文档](http://docs.gitlab.com/ce/)

配置文件路径

/etc/gitlab/gitlab.rb

配置文件修改之后要执行下面语句才能生效

sudo gitlab-ctl reconfigure

启动gitlab所有组件

sudo gitlab-ctl start

停止gitlab所有组件

sudo gitlab-ctl stop

重启gitab所有组件

sudo gitlab-ctl restart

PostgreSQL

http://docs.gitlab.com/omnibus/settings/database.html
进入db
sudo gitlab-rails dbconsole

GitLab恢复备份说明

]]>
<![CDATA[阿琛]]>以下命令可以查看.a文件支持哪些cpu架构 file xxx.a otool -L xxx.a lipo -info xxx.a otool -MVv xxx.a 如果是多架构合一的,可以通过以下命令抽取单个架构的.a文件 lipo xxx.a -thin armv7 -output newName.a 以下命令可以查看.a中使用的方法名列表(方便查找使用私有API的问题) nm xxx.a 以下命令把.a中所有类的方法名列表输出到method.txt文件中 nm xxx.a >> method.txt]]>https://noahzy.com/yi-xie-ming-ling-2/5d775cb67fa7c611a5d555adTue, 10 Sep 2019 08:20:22 GMT以下命令可以查看.a文件支持哪些cpu架构 file xxx.a otool -L xxx.a lipo -info xxx.a otool -MVv xxx.a 如果是多架构合一的,可以通过以下命令抽取单个架构的.a文件 lipo xxx.a -thin armv7 -output newName.a 以下命令可以查看.a中使用的方法名列表(方便查找使用私有API的问题) nm xxx.a 以下命令把.a中所有类的方法名列表输出到method.txt文件中 nm xxx.a >> method.txt]]><![CDATA[阿琛]]>(注:本文转自Git入门教程)

创建新仓库:

git init在当前目录建立本地新仓库

git clone url在当前目录克隆一个远端仓库

添加用户名及邮箱有两种方式

方式一:编辑.git/config

[user]  
	name =  
	email =

方式二:使用命令行

	git config --global user.name "xxxxxx" 
	git config --global user.email "xxxxxxx"
	(备注:--global表示添加到全局配置中,全局配置在~/.gitconfig中, 如果不加--global,就只会修改当前目录下.git/config)

在本地仓库上工作:

你的文件可能存在于不同的层次:

  • 工作目录
  • 暂存区域
  • 本地仓库
  • 工作目录:

    文件可能有三种不同状态:

    ]]>
    https://noahzy.com/zhuan-git-gitru-men-jiao-cheng/5d775ae17fa7c611a5d55591Tue, 10 Sep 2019 08:13:26 GMT

    (注:本文转自Git入门教程)

    创建新仓库:

    git init在当前目录建立本地新仓库

    git clone url在当前目录克隆一个远端仓库

    添加用户名及邮箱有两种方式

    方式一:编辑.git/config

    [user]  
    	name =  
    	email =
    

    方式二:使用命令行

    	git config --global user.name "xxxxxx" 
    	git config --global user.email "xxxxxxx"
    	(备注:--global表示添加到全局配置中,全局配置在~/.gitconfig中, 如果不加--global,就只会修改当前目录下.git/config)
    

    在本地仓库上工作:

    你的文件可能存在于不同的层次:

  • 工作目录
  • 暂存区域
  • 本地仓库
  • 工作目录:

    文件可能有三种不同状态:

  • 未被追踪
  • 已追踪未修改
  • 已追踪已修改
  • 暂存区域(包含了在下次将要被提交的修改):

    使用git add/rm <file>将增加/修改/删除的文件暂存

    本地仓库:

    使用git commit -m "message"提交已经暂存的更改

    常用操作:

    git status:显示当前所处分支与修改(包括暂存与未暂存)

    git checkout HEAD -- <file> 使某个文件恢复到上次提交时的状态

    git checkout -- <file> 使某个文件恢复到上次暂存时的状态(Updated Lucups@V2EX

    git reset HEAD <file> 使某个修改由已暂存变为未暂存

    将本地仓库重置成与远端一样:git fetch origin git reset --hard origin/master

    检查提交历史:

    git log:查看以往的提交以及提交时的留言

    git log 1 -p:查看最后一次提交及其改动

    git log --author=<name>:仅显示某人的的提交

    git log --pretty=oneline:一行显示一个提交

    git log --graph --oneline --decorate --all:以树状图显示提交历史

    git log --name-status:仅显示哪些文件被改动

    暂存:

    当你需要建立新分支时,可能需要一个干净的工作目录,但是一时半会儿没法提交时,暂存是你的好选择。

    git stash:暂存当前的暂存区域

    git stash list:列出已有的暂存

    git stash pop:将暂存中的改变重新释放出来

    分支与合并:

    git branch <branchname>来创建新分支

    优点:
    1. 安全:多个特性平行开发,再也不会搞得一塌糊涂
    2. 灵活的共享:只要不主动推送,本地新分支是不会推送至远端的
    3. 切换十分方便
    何时创建新分支:

    新的特性需要开发,有Bug需要修补,不同思路做实验。。。

    HEAD:

    Git中只有一个分支是活跃的也就是HEAD所指向的那个。

    使用git checkout <branch>来切换分支,HEAD也随之改变

    git branch -v:列出所有分支以及当前活跃的分支

    合并:

    git merge <branchname>:将某分支合并到当前分支

    git branch -d <branchname>:删除某分支

    远端仓库:

    git push origin <branch>:推送本地分支

    git fetch <branch>:获取远端分支但不进行合并

    合并前可以通过git diff <source_branch> <target_branch>检查更改

    git pull:获取远端分支并尝试合并

    增加远端仓库:

    git remote add shortname <url>

    同步远端的分支list

    (例如某分支在远端被删除了)
    git fetch -p
    or
    git remote prune origin

    打标签:

    发布新版本时,打标签是个不错的选择:git tag <tagname> <hash>

    GIT PUSH/PULL时总需要输入用户名密码的解决方案

    git操作时只要输入一次用户名与密码,以后就不用输入了
    在 mac系统
    git config --global credential.helper osxkeychain
    在其他系统
    git config --global credential.helper store

    小提示:

    gitk:内置的图形界面

    git config color.ui true:使命令行输出变成彩色

    git add -i:不用一个个输入需要暂存的文件的文件名,进行交互式选择

    客户端与拓展阅读:

    客户端:

    Tower(Mac)

    GitHub(Windows&Mac)

    SourceTree(Windows&Mac)

    拓展阅读:

    图解Git

    Pro Git 中文第一版 英文第二版

    Think Like (a) Git

    ]]>
    <![CDATA[阿琛]]>实现的效果如下

    简单来说就是利用了  layer的mask遮罩 + 贝塞尔曲线UIBezierPath

    (PS:恕我偷个懒 教程以后有空再补上)

    先上Demo代码

    ]]>
    https://noahzy.com/ios-shi-xian-gua-gua-le-xiao-guo/5d7757387fa7c611a5d55583Tue, 10 Sep 2019 07:58:43 GMT实现的效果如下

    简单来说就是利用了  layer的mask遮罩 + 贝塞尔曲线UIBezierPath

    (PS:恕我偷个懒 教程以后有空再补上)

    先上Demo代码

    ]]>
    <![CDATA[阿琛]]>一、在iOS的某些系统控件中(例如:UITextView、UIWebView),自带有放大镜效果,就是长按住某些文字,然后就会弹出一个放大框显示放大后的文字。

    例如下面是系统的效果

    系统自带的效果

    二、下面我们来自己实现这个效果

    实现的思路,简单来说就是  
    1.在touchesBegan和touchesMoved中截取整个屏幕生成UIImage A
    2.根据touch点的坐标从图片A中截取对应部分生成图片B(使用CGImageCreateWithImageInRect方法)  
    3.将B放到一个UIImageView展示到keyWindows中,根据touch点的坐标来计算UIImageView显示的位置
    4.最后在touchesEnded里面记得隐藏UIImageView。
    

    下面的是实现后的效果

    自制效果

    最后附上Demo代码地址 2016.4.17

    放大镜Demo

    ]]>
    https://noahzy.com/ios-zi-zhi-fang-da-jing-xiao-guo/5d7755217fa7c611a5d55572Tue, 10 Sep 2019 07:48:55 GMT一、在iOS的某些系统控件中(例如:UITextView、UIWebView),自带有放大镜效果,就是长按住某些文字,然后就会弹出一个放大框显示放大后的文字。

    例如下面是系统的效果

    系统自带的效果

    二、下面我们来自己实现这个效果

    实现的思路,简单来说就是  
    1.在touchesBegan和touchesMoved中截取整个屏幕生成UIImage A
    2.根据touch点的坐标从图片A中截取对应部分生成图片B(使用CGImageCreateWithImageInRect方法)  
    3.将B放到一个UIImageView展示到keyWindows中,根据touch点的坐标来计算UIImageView显示的位置
    4.最后在touchesEnded里面记得隐藏UIImageView。
    

    下面的是实现后的效果

    自制效果

    最后附上Demo代码地址 2016.4.17

    放大镜Demo

    ]]>
    <![CDATA[阿琛]]>最近闲来无聊在 关于页面中 留了个彩蛋,隐藏了一个动画在其中。

    动画Demo

    动画使用了JHChainableAnimations

    该库使用链式语法调用,非常简介明了,而且支持缓动函数,对于做一些模拟重力,弹簧等运动非常有用

    缓动函数:指定动画效果在执行时的速度,使其看起来更加真实。
    现实物体照着一定节奏移动,并不是一开始就移动很快的。当我们打开抽屉时,首先会让它加速,然后慢下来。当某个东西往下掉时,首先是越掉越快,撞到地上后回弹,最终才又碰触地板。

    以下就是缓动函数

    缓动函数
    ]]>
    https://noahzy.com/iosdong-hua-cai-dan/5d7753f57fa7c611a5d5555fTue, 10 Sep 2019 07:44:26 GMT最近闲来无聊在 关于页面中 留了个彩蛋,隐藏了一个动画在其中。

    动画Demo

    动画使用了JHChainableAnimations

    该库使用链式语法调用,非常简介明了,而且支持缓动函数,对于做一些模拟重力,弹簧等运动非常有用

    缓动函数:指定动画效果在执行时的速度,使其看起来更加真实。
    现实物体照着一定节奏移动,并不是一开始就移动很快的。当我们打开抽屉时,首先会让它加速,然后慢下来。当某个东西往下掉时,首先是越掉越快,撞到地上后回弹,最终才又碰触地板。

    以下就是缓动函数

    缓动函数
    ]]>
    <![CDATA[阿琛]]>记录下各个页面的跳转URL Scheme,方便以后查询

    WIFI prefs:root=WIFI
    个人热点 prefs:root=INTERNET_TETHERING
    蜂窝设置 prefs:root=MOBILE_DATA_SETTINGS_ID
    隐私 prefs:root=Privacy
    隐私-相机 prefs:root=Privacy&path=CAMERA
    隐私-相册 prefs:root=Privacy&path=PHOTOS
    隐私-定位服务 prefs:root=LOCATION_SERVICES
    通用 prefs:root=General&path
    VPN prefs:
    ]]>
    https://noahzy.com/ios-ji-lu-settingde-tiao-zhuan-url-scheme/5d773df17fa7c611a5d5553eTue, 10 Sep 2019 06:09:48 GMT记录下各个页面的跳转URL Scheme,方便以后查询

    WIFI prefs:root=WIFI
    个人热点 prefs:root=INTERNET_TETHERING
    蜂窝设置 prefs:root=MOBILE_DATA_SETTINGS_ID
    隐私 prefs:root=Privacy
    隐私-相机 prefs:root=Privacy&path=CAMERA
    隐私-相册 prefs:root=Privacy&path=PHOTOS
    隐私-定位服务 prefs:root=LOCATION_SERVICES
    通用 prefs:root=General&path
    VPN prefs:root=VPN
    关于 prefs:root=General&path=About
    辅助功能 prefs:root=General&path=ACCESSIBILITY
    飞行模式 prefs:root=AIRPLANE_MODE
    自动锁屏时间 prefs:root=General&path=AUTOLOCK
    用量 prefs:root=General&path=USAGE
    亮度 prefs:root=Brightness
    蓝牙 prefs:root=General&path=Bluetooth
    日期和时间 prefs:root=General&path=DATE_AND_TIME
    Facetime设置 prefs:root=FACETIME
    键盘设置 prefs:root=General&path=Keyboard
    icloud prefs:root=CASTLE
    备份 prefs:root=CASTLE&path=STORAGE_AND_BACKUP
    语言与地区设置 prefs:root=General&path=INTERNATIONAL
    账户设置 prefs:root=ACCOUNT_SETTINGS
    音乐 prefs:root=MUSICEQ
    均衡器 prefs:root=MUSIC&path=EQ
    设置 prefs:root=
    备忘录 prefs:root=NOTES
    通知 prefs:root=NOTIFICATIONS_ID
    电话设置 prefs:root=Phone
    照片与相机设置 prefs:root=Photos
    还原 prefs:root=General&path=Reset
    铃声设置 prefs:root=Sounds&path=Ringtone
    Safari prefs:root=Safari
    声音 prefs:root=Sounds
    系统更新 prefs:root=General&path=SOFTWARE_UPDATE_LINKSTORE
    设置 prefs:root=STORE
    视频设置 prefs:root=VIDEO
    壁纸设置 prefs:root=Wallpaper
    

    代码可参考:iOS应用内跳转到系统设置页面

    ]]>
    <![CDATA[阿琛]]>本文转载自:H5 类似雷竞技app机制浅析 - 移动端 Web 加载性能优化

    1. H5 类似雷竞技app机制介绍

    H5,即 HTML5,是新一代的 HTML 标准,加入很多新的特性。离线存储(也可称为类似雷竞技app机制)是其中一个非常重要的特性。H5 引入的离线存储,这意味着 web 应用可进行类似雷竞技app,并可在没有因特网连接时进行访问。

    H5 应用程序类似雷竞技app为应用带来三个优势:

  • 离线浏览 用户可在应用离线时使用它们
  • 速度 已类似雷竞技app资源加载得更快
  • 减少服务器负载 浏览器将只从服务器下载更新过或更改过的资源。
  • 根据标准,到目前为止,H5 一共有6种类似雷竞技app机制,有些是之前已有,有些是 H5 才新加入的。

    1. 浏览器类似雷竞技app机制
    2. Dom Storgage(Web Storage)存储机制
    3. Web SQL
    ]]>
    https://noahzy.com/zhuan-qian-duan-huan-cun-h5-huan-cun-ji-zhi-qian-xi-yi-dong-duan-web-jia-zai-xing-neng-you-hua/5d771aab7fa7c611a5d554f6Tue, 10 Sep 2019 03:52:49 GMT

    本文转载自:H5 类似雷竞技app机制浅析 - 移动端 Web 加载性能优化

    1. H5 类似雷竞技app机制介绍

    H5,即 HTML5,是新一代的 HTML 标准,加入很多新的特性。离线存储(也可称为类似雷竞技app机制)是其中一个非常重要的特性。H5 引入的离线存储,这意味着 web 应用可进行类似雷竞技app,并可在没有因特网连接时进行访问。

    H5 应用程序类似雷竞技app为应用带来三个优势:

  • 离线浏览 用户可在应用离线时使用它们
  • 速度 已类似雷竞技app资源加载得更快
  • 减少服务器负载 浏览器将只从服务器下载更新过或更改过的资源。
  • 根据标准,到目前为止,H5 一共有6种类似雷竞技app机制,有些是之前已有,有些是 H5 才新加入的。

    1. 浏览器类似雷竞技app机制
    2. Dom Storgage(Web Storage)存储机制
    3. Web SQL Database 存储机制
    4. Application Cache(AppCache)机制
    5. Indexed Database (IndexedDB)
    6. File System API

    下面我们首先分析各种类似雷竞技app机制的原理、用法及特点;然后针对 Anroid 移动端 Web 性能加载优化的需求,看如果利用适当类似雷竞技app机制来提高 Web 的加载性能。

    2. H5 类似雷竞技app机制原理分析

    2.1 浏览器类似雷竞技app机制

    浏览器类似雷竞技app机制是指通过 HTTP 协议头里的 Cache-Control(或 Expires)和 Last-Modified(或 Etag)等字段来控制文件类似雷竞技app的机制。这应该是 WEB 中最早的类似雷竞技app机制了,是在 HTTP 协议中实现的,有点不同于 Dom Storage、AppCache 等类似雷竞技app机制,但本质上是一样的。可以理解为,一个是协议层实现的,一个是应用层实现的。

    Cache-Control 用于控制文件在本地类似雷竞技app有效时长。最常见的,比如服务器回包:Cache-Control:max-age=600 表示文件在本地应该类似雷竞技app,且有效时长是600秒(从发出请求算起)。在接下来600秒内,如果有请求这个资源,浏览器不会发出 HTTP 请求,而是直接使用本地类似雷竞技app的文件。

    Last-Modified 是标识文件在服务器上的最新更新时间。下次请求时,如果文件类似雷竞技app过期,浏览器通过 If-Modified-Since 字段带上这个时间,发送给服务器,由服务器比较时间戳来判断文件是否有修改。如果没有修改,服务器返回304告诉浏览器继续使用类似雷竞技app;如果有修改,则返回200,同时返回最新的文件。

    Cache-Control 通常与 Last-Modified 一起使用。一个用于控制类似雷竞技app有效时间,一个在类似雷竞技app失效后,向服务查询是否有更新。

    Cache-Control 还有一个同功能的字段:Expires。Expires 的值一个绝对的时间点,如:Expires: Thu, 10 Nov 2015 08:45:11 GMT,表示在这个时间点之前,类似雷竞技app都是有效的。

    Expires 是 HTTP1.0 标准中的字段,Cache-Control 是 HTTP1.1 标准中新加的字段,功能一样,都是控制类似雷竞技app的有效时间。当这两个字段同时出现时,Cache-Control 是高优化级的。

    Etag 也是和 Last-Modified 一样,对文件进行标识的字段。不同的是,Etag 的取值是一个对文件进行标识的特征字串。在向服务器查询文件是否有更新时,浏览器通过 If-None-Match 字段把特征字串发送给服务器,由服务器和文件最新特征字串进行匹配,来判断文件是否有更新。没有更新回包304,有更新回包200。Etag 和 Last-Modified 可根据需求使用一个或两个同时使用。两个同时使用时,只要满足基中一个条件,就认为文件没有更新。

    另外有两种特殊的情况:

  • 手动刷新页面(F5),浏览器会直接认为类似雷竞技app已经过期(可能类似雷竞技app还没有过期),在请求中加上字段:Cache-Control:max-age=0,发包向服务器查询是否有文件是否有更新。
  • 强制刷新页面(Ctrl+F5),浏览器会直接忽略本地的类似雷竞技app(有类似雷竞技app也会认为本地没有类似雷竞技app),在请求中加上字段:Cache-Control:no-cache(或 Pragma:no-cache),发包向服务重新拉取文件。
  • 下面是通过 Google Chrome 浏览器(用其他浏览器+抓包工具也可以)自带的开发者工具,对一个资源文件不同情况请求与回包的截图。

    首次请求:200

    【转】【前端类似雷竞技app】H5 类似雷竞技app机制浅析 - 移动端 Web 加载性能优化

    类似雷竞技app有效期内请求:200(from cache)

    【转】【前端类似雷竞技app】H5 类似雷竞技app机制浅析 - 移动端 Web 加载性能优化

    类似雷竞技app过期后请求:304(Not Modified)

    【转】【前端类似雷竞技app】H5 类似雷竞技app机制浅析 - 移动端 Web 加载性能优化

    一般浏览器会将类似雷竞技app记录及类似雷竞技app文件存在本地 Cache 文件夹中。Android 下 App 如果使用 Webview,类似雷竞技app的文件记录及文件内容会存在当前 app 的 data 目录中。

    分析:Cache-Control 和 Last-Modified 一般用在 Web 的静态资源文件上,如 JS、CSS 和一些图像文件。通过设置资源文件类似雷竞技app属性,对提高资源文件加载速度,节省流量很有意义,特别是移动网络环境。但问题是:类似雷竞技app有效时长该如何设置?如果设置太短,就起不到类似雷竞技app的使用;如果设置的太长,在资源文件有更新时,浏览器如果有类似雷竞技app,则不能及时取到最新的文件。

    Last-Modified 需要向服务器发起查询请求,才能知道资源文件有没有更新。虽然服务器可能返回304告诉没有更新,但也还有一个请求的过程。对于移动网络,这个请求可能是比较耗时的。有一种说法叫“消灭304”,指的就是优化掉304的请求。

    抓包发现,带 if-Modified-Since 字段的请求,如果服务器回包304,回包带有 Cache-Control:max-age 或 Expires 字段,文件的类似雷竞技app有效时间会更新,就是文件的类似雷竞技app会重新有效。304回包后如果再请求,则又直接使用类似雷竞技app文件了,不再向服务器查询文件是否更新了,除非新的类似雷竞技app时间再次过期。

    另外,Cache-Control 与 Last-Modified 是浏览器内核的机制,一般都是标准的实现,不能更改或设置。以 QQ 浏览器的 X5为例,Cache-Control 与 Last-Modified 类似雷竞技app不能禁用。类似雷竞技app容量是12MB,不分HOST,过期的类似雷竞技app会最先被清除。如果都没过期,应该优先清最早的类似雷竞技app或最快到期的或文件大小最大的;过期类似雷竞技app也有可能还是有效的,清除类似雷竞技app会导致资源文件的重新拉取。

    还有,浏览器,如 X5,在使用类似雷竞技app文件时,是没有对类似雷竞技app文件内容进行校验的,这样类似雷竞技app文件内容被修改的可能。

    分析发现,浏览器的类似雷竞技app机制还不是非常完美的类似雷竞技app机制。完美的类似雷竞技app机制应该是这样的:

    1. 类似雷竞技app文件没更新,尽可能使用类似雷竞技app,不用和服务器交互;
    2. 类似雷竞技app文件有更新时,第一时间能使用到新的文件;
    3. 类似雷竞技app的文件要保持完整性,不使用被修改过的类似雷竞技app文件;
    4. 类似雷竞技app的容量大小要能设置或控制,类似雷竞技app文件不能因为存储空间限制或过期被清除。
      以X5为例,第1、2条不能同时满足,第3、4条都不能满足。

    在实际应用中,为了解决 Cache-Control 类似雷竞技app时长不好设置的问题,以及为了”消灭304“,Web前端采用的方式是:

    1. 在要类似雷竞技app的资源文件名中加上版本号或文件 MD5值字串,如 common.d5d02a02.js,common.v1.js,同时设置 Cache-Control:max-age=31536000,也就是一年。在一年时间内,资源文件如果本地有类似雷竞技app,就会使用类似雷竞技app;也就不会有304的回包。
    2. 如果资源文件有修改,则更新文件内容,同时修改资源文件名,如 common.v2.js,html页面也会引用新的资源文件名。

    通过这种方式,实现了:类似雷竞技app文件没有更新,则使用类似雷竞技app;类似雷竞技app文件有更新,则第一时间使用最新文件的目的。即上面说的第1、2条。第3、4条由于浏览器内部机制,目前还无法满足。

    2.2 Dom Storage 存储机制

    DOM 存储是一套在 Web Applications 1.0 规范中首次引入的与存储相关的特性的总称,现在已经分离出来,单独发展成为独立的 W3C Web 存储规范。 DOM 存储被设计为用来提供一个更大存储量、更安全、更便捷的存储方法,从而可以代替掉将一些不需要让服务器知道的信息存储到 cookies 里的这种传统方法。

    上面一段是对 Dom Storage 存储机制的官方表述。看起来,Dom Storage 机制类似 Cookies,但有一些优势。

    Dom Storage 是通过存储字符串的 Key/Value 对来提供的,并提供 5MB (不同浏览器可能不同,分 HOST)的存储空间(Cookies 才 4KB)。另外 Dom Storage 存储的数据在本地,不像 Cookies,每次请求一次页面,Cookies 都会发送给服务器。

    DOM Storage 分为 sessionStorage 和 localStorage。localStorage 对象和 sessionStorage 对象使用方法基本相同,它们的区别在于作用的范围不同。sessionStorage 用来存储与页面相关的数据,它在页面关闭后无法使用。而 localStorage 则持久存在,在页面关闭后也可以使用。

    Dom Storage 提供了以下的存储接口:

    interface Storage { 
    readonly attribute unsigned long length; 
    [IndexGetter] DOMString key(in unsigned long index); 
    [NameGetter] DOMString getItem(in DOMString key); 
    [NameSetter] void setItem(in DOMString key, in DOMString data); 
    [NameDeleter] void removeItem(in DOMString key); 
    void clear();
    };

    sessionStorage 是个全局对象,它维护着在页面会话(page session)期间有效的存储空间。只要浏览器开着,页面会话周期就会一直持续。当页面重新载入(reload)或者被恢复(restores)时,页面会话也是一直存在的。每在新标签或者新窗口中打开一个新页面,都会初始化一个新的会话。

    <script type="text/javascript">
     // 当页面刷新时,从sessionStorage恢复之前输入的内容
     window.onload = function(){
        if (window.sessionStorage) {
            var name = window.sessionStorage.getItem("name");
            if (name != "" || name != null){
                document.getElementById("name").value = name;
             }
         }
     };
    
     // 将数据保存到sessionStorage对象中
     function saveToStorage() {
        if (window.sessionStorage) {
            var name = document.getElementById("name").value;
            window.sessionStorage.setItem("name", name);
            window.location.href="tfttjpo_tupsbhf.iunm";
         }
     }
     </script>
    
    <form action="./session_storage.html">
        <input type="text" name="name" id="name"/>
        <input type="button" value="Save" onclick="saveToStorage()"/>
    </form>
    

    当浏览器被意外刷新的时候,一些临时数据应当被保存和恢复。sessionStorage 对象在处理这种情况的时候是最有用的。比如恢复我们在表单中已经填写的数据。

    把上面的代码复制到 session_storage.html(也可以从附件中直接下载)页面中,用 Google Chrome 浏览器的不同 PAGE 或 WINDOW 打开,在输入框中分别输入不同的文字,再点击“Save”,然后分别刷新。每个 PAGE 或 WINDOW 显示都是当前PAGE输入的内容,互不影响。关闭 PAGE,再重新打开,上一次输入保存的内容已经没有了。

    【转】【前端类似雷竞技app】H5 类似雷竞技app机制浅析 - 移动端 Web 加载性能优化
    【转】【前端类似雷竞技app】H5 类似雷竞技app机制浅析 - 移动端 Web 加载性能优化

    Local Storage 的接口、用法与 Session Storage 一样,唯一不同的是:Local Storage 保存的数据是持久性的。当前 PAGE 关闭(Page Session 结束后),保存的数据依然存在。重新打开PAGE,上次保存的数据可以获取到。另外,Local Storage 是全局性的,同时打开两个 PAGE 会共享一份存数据,在一个PAGE中修改数据,另一个 PAGE 中是可以感知到的。

    <script>
      //通过localStorage直接引用key, 另一种写法,等价于:
      //localStorage.getItem("pageLoadCount");
      //localStorage.setItem("pageLoadCount", value);
      if (!localStorage.pageLoadCount)
    localStorage.pageLoadCount = 0;
         localStorage.pageLoadCount = parseInt(localStorage.pageLoadCount) + 1;
         document.getElementById('count').textContent = localStorage.pageLoadCount;
    </script>
    
    <p>
        You have viewed this page
        <span id="count">an untold number of</span>
        time(s).
    </p> 
    

    将上面代码复制到 local_storage.html 的页面中,用浏览器打开,pageLoadCount 的值是1;关闭 PAGE 重新打开,pageLoadCount 的值是2。这是因为第一次的值已经保存了。

    【转】【前端类似雷竞技app】H5 类似雷竞技app机制浅析 - 移动端 Web 加载性能优化
    【转】【前端类似雷竞技app】H5 类似雷竞技app机制浅析 - 移动端 Web 加载性能优化

    用两个 PAGE 同时打开 local_storage.html,并分别交替刷新,发现两个 PAGE 是共享一个 pageLoadCount 的。

    【转】【前端类似雷竞技app】H5 类似雷竞技app机制浅析 - 移动端 Web 加载性能优化
    【转】【前端类似雷竞技app】H5 类似雷竞技app机制浅析 - 移动端 Web 加载性能优化

    分析:Dom Storage 给 Web 提供了一种更录活的数据存储方式,存储空间更大(相对 Cookies),用法也比较简单,方便存储服务器或本地的一些临时数据。

    从 DomStorage 提供的接口来看,DomStorage 适合存储比较简单的数据,如果要存储结构化的数据,可能要借助 JASON了,将要存储的对象转为 JASON 字串。不太适合存储比较复杂或存储空间要求比较大的数据,也不适合存储静态的文件等。

    在 Android 内嵌 Webview 中,需要通过 Webview 设置接口启用 Dom Storage。

    WebView myWebView = (WebView) findViewById(R.id.webview);
    WebSettings webSettings = myWebView.getSettings();
    webSettings.setDomStorageEnabled(true);
    

    拿 Android 类比的话,Web 的 Dom Storage 机制类似于 Android 的 SharedPreference 机制。

    2.3 Web SQL Database存储机制

    H5 也提供基于 SQL 的数据库存储机制,用于存储适合数据库的结构化数据。根据官方的标准文档,Web SQL Database 存储机制不再推荐使用,将来也不再维护,而是推荐使用 AppCache 和 IndexedDB。

    现在主流的浏览器(点击查看浏览器支持情况)都还是支持 Web SQL Database 存储机制的。Web SQL Database 存储机制提供了一组 API 供 Web App 创建、存储、查询数据库。

    下面通过简单的例子,演示下 Web SQL Database 的使用。

    <script>
        if(window.openDatabase){
          //打开数据库,如果没有则创建
          var db = openDatabase('mydb', '1.0', 'Test DB', 2 * 1024);
    
           //通过事务,创建一个表,并添加两条记录
          db.transaction(function (tx) {
               tx.executeSql('CREATE TABLE IF NOT EXISTS LOGS (id unique, log)');
               tx.executeSql('INSERT INTO LOGS (id, log) VALUES (1, "foobar")');
               tx.executeSql('INSERT INTO LOGS (id, log) VALUES (2, "logmsg")');
           });
    
          //查询表中所有记录,并展示出来
         db.transaction(function (tx) {
             tx.executeSql('SELECT * FROM LOGS', [], function (tx, results) {
                 var len = results.rows.length, i;
                 msg = "<p>Found rows: " + len + "</p>";
                 for(i=0; i<len; i++){
                     msg += "<p>" + results.rows.item(i).log + "</p>";
                 }
                 document.querySelector('#status').innerHTML =  msg;
                 }, null);
          });
    }
    
    </script>
    
    <div id="status" name="status">Status Message</div>
    

    将上面代码复制到 sql_database.html 中,用浏览器打开,可看到下面的内容。

    【转】【前端类似雷竞技app】H5 类似雷竞技app机制浅析 - 移动端 Web 加载性能优化

    官方建议浏览器在实现时,对每个 HOST 的数据库存储空间作一定限制,建议默认是 5MB(分 HOST)的配额;达到上限后,可以申请更多存储空间。另外,现在主流浏览器 SQL Database 的实现都是基于 SQLite。

    分析:SQL Database 的主要优势在于能够存储结构复杂的数据,能充分利用数据库的优势,可方便对数据进行增加、删除、修改、查询。由于 SQL 语法的复杂性,使用起来麻烦一些。SQL Database 也不太适合做静态文件的类似雷竞技app。

    在 Android 内嵌 Webview 中,需要通过 Webview 设置接口启用 SQL Database,同时还要设置数据库文件的存储路径。

    WebView myWebView = (WebView) findViewById(R.id.webview);
    WebSettings webSettings = myWebView.getSettings();
    webSettings.setDatabaseEnabled(true);
    final String dbPath = getApplicationContext().getDir("db", Context.MODE_PRIVATE).getPath();
    webSettings.setDatabasePath(dbPath); 
    

    Android 系统也使用了大量的数据库用来存储数据,比如联系人、短消息等;数据库的格式也 SQLite。Android 也提供了 API 来操作 SQLite。Web SQL Database 存储机制就是通过提供一组 API,借助浏览器的实现,将这种 Native 的功能提供给了 Web App。

    2.4 Application Cache 机制

    Application Cache(简称 AppCache)似乎是为支持 Web App 离线使用而开发的类似雷竞技app机制。它的类似雷竞技app机制类似于浏览器的类似雷竞技app(Cache-Control 和 Last-Modified)机制,都是以文件为单位进行类似雷竞技app,且文件有一定更新机制。但 AppCache 是对浏览器类似雷竞技app机制的补充,不是替代。

    先拿 W3C 官方的一个例子,说下 AppCache 机制的用法与功能。

    <!DOCTYPE html>
    <html manifest="demo_html.appcache">
    <body>
    
    <script src="demo_time.js"></script>
    
    <p id="timePara"><button onclick="getDateTime()">Get Date and Time</button></p>
    <p><img src="http://www.noahzy.com/img_logo.gif" width="336" height="69"></p>
    <p>Try opening <a href="usziunm5_iunm_nbojgftu.iun" target="_blank">this page</a>, then go offline, and reload the page. The script and the image should still work.</p>
    
    </body>
    </html>
    

    上面 HTML 文档,引用外部一个 JS 文件和一个 GIF 图片文件,在其 HTML 头中通过 manifest 属性引用了一个 appcache 结尾的文件。

    我们在 Google Chrome 浏览器中打开这个 HTML 链接,JS 功能正常,图片也显示正常。禁用网络,关闭浏览器重新打开这个链接,发现 JS 工作正常,图片也显示正常。当然也有可能是浏览类似雷竞技app起的作用,我们可以在文件的浏览器类似雷竞技app过期后,禁用网络再试,发现 HTML 页面也是正常的。

    通过 Google Chrome 浏览器自带的工具,我们可以查看已经类似雷竞技app的 AppCache(分 HOST)。

    【转】【前端类似雷竞技app】H5 类似雷竞技app机制浅析 - 移动端 Web 加载性能优化

    上面截图中的类似雷竞技app,就是我们刚才打开 HTML 的页面 AppCache。从截图中看,HTML 页面及 HTML 引用的 JS、GIF 图像文件都被类似雷竞技app了;另外 HTML 头中 manifest 属性引用的 appcache 文件也类似雷竞技app了。

    AppCache 的原理有两个关键点:manifest 属性和 manifest 文件。

    HTML 在头中通过 manifest 属性引用 manifest 文件。manifest 文件,就是上面以 appcache 结尾的文件,是一个普通文件文件,列出了需要类似雷竞技app的文件。

    【转】【前端类似雷竞技app】H5 类似雷竞技app机制浅析 - 移动端 Web 加载性能优化

    上面截图中的 manifest 文件,就 HTML 代码引用的 manifest 文件。文件比较简单,第一行是关键字,第二、三行就是要类似雷竞技app的文件路径(相对路径)。这只是最简单的 manifest 文件,完整的还包括其他关键字与内容。引用 manifest 文件的 HTML 和 manifest 文件中列出的要类似雷竞技app的文件最终都会被浏览器类似雷竞技app。

    完整的 manifest 文件,包括三个 Section,类型 Windows 中 ini 配置文件的 Section,不过不要中括号。

    1. CACHE MANIFEST - Files listed under this header will be cached after they are downloaded for the first time
    2. NETWORK - Files listed under this header require a connection to the server, and will never be cached
    3. FALLBACK - Files listed under this header specifies fallback pages if a page is inaccessible

    完整的 manifest 文件,如:

    CACHE MANIFEST
    # 2012-02-21 v1.0.0
    /theme.css
    /logo.gif
    /main.js
    
    NETWORK:
    login.asp
    
    FALLBACK:
    /html/ /offline.html 
    

    总的来说,浏览器在首次加载 HTML 文件时,会解析 manifest 属性,并读取 manifest 文件,获取 Section:CACHE MANIFEST 下要类似雷竞技app的文件列表,再对文件类似雷竞技app。

    AppCache 的类似雷竞技app文件,与浏览器的类似雷竞技app文件分开存储的,还是一份?应该是分开的。因为 AppCache 在本地也有 5MB(分 HOST)的空间限制。

    AppCache 在首次加载生成后,也有更新机制。被类似雷竞技app的文件如果要更新,需要更新 manifest 文件。因为浏览器在下次加载时,除了会默认使用类似雷竞技app外,还会在后台检查 manifest 文件有没有修改(byte by byte)。发现有修改,就会重新获取 manifest 文件,对 Section:CACHE MANIFEST 下文件列表检查更新。manifest 文件与类似雷竞技app文件的检查更新也遵守浏览器类似雷竞技app机制。

    如用用户手动清了 AppCache 类似雷竞技app,下次加载时,浏览器会重新生成类似雷竞技app,也可算是一种类似雷竞技app的更新。另外, Web App 也可用代码实现类似雷竞技app更新。

    分析:AppCache 看起来是一种比较好的类似雷竞技app方法,除了类似雷竞技app静态资源文件外,也适合构建 Web 离线 App。在实际使用中有些需要注意的地方,有一些可以说是”坑“。

    1. 要更新类似雷竞技app的文件,需要更新包含它的 manifest 文件,那怕只加一个空格。常用的方法,是修改 manifest 文件注释中的版本号。如:# 2012-02-21 v1.0.0
    2. 被类似雷竞技app的文件,浏览器是先使用,再通过检查 manifest 文件是否有更新来更新类似雷竞技app文件。这样类似雷竞技app文件可能用的不是最新的版本。
    3. 在更新类似雷竞技app过程中,如果有一个文件更新失败,则整个更新会失败。
    4. manifest 和引用它的HTML要在相同 HOST。
    5. manifest 文件中的文件列表,如果是相对路径,则是相对 manifest 文件的相对路径。
    6. manifest 也有可能更新出错,导致类似雷竞技app文件更新失败。
    7. 没有类似雷竞技app的资源在已经类似雷竞技app的 HTML 中不能加载,即使有网络。例如:http://appcache-demo.s3-website-us-east-1.amazonaws.com/without-network/
    8. manifest 文件本身不能被类似雷竞技app,且 manifest 文件的更新使用的是浏览器类似雷竞技app机制。所以 manifest 文件的 Cache-Control 类似雷竞技app时间不能设置太长。

    另外,根据官方文档,AppCache 已经不推荐使用了,标准也不会再支持。现在主流的浏览器都是还支持 AppCache的,以后就不太确定了。

    在Android 内嵌 Webview中,需要通过 Webview 设置接口启用 AppCache,同时还要设置类似雷竞技app文件的存储路径,另外还可以设置类似雷竞技app的空间大小。

    WebView myWebView = (WebView) findViewById(R.id.webview);
    WebSettings webSettings = myWebView.getSettings();
    webSettings.setAppCacheEnabled(true);
    final String cachePath = getApplicationContext().getDir("cache", Context.MODE_PRIVATE).getPath();
    webSettings.setAppCachePath(cachePath);
    webSettings.setAppCacheMaxSize(5*1024*1024);
    

    2.5 Indexed Database

    IndexedDB 也是一种数据库的存储机制,但不同于已经不再支持的 Web SQL Database。IndexedDB 不是传统的关系数据库,可归为 NoSQL 数据库。IndexedDB 又类似于 Dom Storage 的 key-value 的存储方式,但功能更强大,且存储空间更大。

    IndexedDB 存储数据是 key-value 的形式。Key 是必需,且要唯一;Key 可以自己定义,也可由系统自动生成。Value 也是必需的,但 Value 非常灵活,可以是任何类型的对象。一般 Value 都是通过 Key 来存取的。

    IndexedDB 提供了一组 API,可以进行数据存、取以及遍历。这些 API 都是异步的,操作的结果都是在回调中返回。

    下面代码演示了 IndexedDB 中 DB 的打开(创建)、存储对象(可理解成有关系数据的”表“)的创建及数据存取、遍历基本功能。

    <script type="text/javascript">
    
    var db;
    
    window.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
    
    //浏览器是否支持IndexedDB
    if (window.indexedDB) {
       //打开数据库,如果没有,则创建
       var openRequest = window.indexedDB.open("people_db", 1);
    
       //DB版本设置或升级时回调
       openRequest.onupgradeneeded = function(e) {
           console.log("Upgrading...");
    
           var thisDB = e.target.result;
           if(!thisDB.objectStoreNames.contains("people")) {
               console.log("Create Object Store: people.");
    
               //创建存储对象,类似于关系数据库的表
               thisDB.createObjectStore("people", { autoIncrement:true });
    
              //创建存储对象, 还创建索引
              //var objectStore = thisDB.createObjectStore("people",{ autoIncrement:true });
             // //first arg is name of index, second is the path (col);
            //objectStore.createIndex("name","name", {unique:false});
           //objectStore.createIndex("email","email", {unique:true});
         }
    }
    
    //DB成功打开回调
    openRequest.onsuccess = function(e) {
        console.log("Success!");
    
        //保存全局的数据库对象,后面会用到
        db = e.target.result;
    
       //绑定按钮点击事件
         document.querySelector("#addButton").addEventListener("click", addPerson, false);
    
        document.querySelector("#getButton").addEventListener("click", getPerson, false);
    
        document.querySelector("#getAllButton").addEventListener("click", getPeople, false);
    
        document.querySelector("#getByName").addEventListener("click", getPeopleByNameIndex1, false);
    }
    
      //DB打开失败回调
      openRequest.onerror = function(e) {
          console.log("Error");
          console.dir(e);
       }
    
    }else{
        alert('Sorry! Your browser doesn\'t support the IndexedDB.');
    }
    
    //添加一条记录
    function addPerson(e) {
        var name = document.querySelector("#name").value;
        var email = document.querySelector("#email").value;
    
        console.log("About to add "+name+"/"+email);
    
        var transaction = db.transaction(["people"],"readwrite");
    var store = transaction.objectStore("people");
    
       //Define a person
       var person = {
           name:name,
           email:email,
           created:new Date()
       }
    
       //Perform the add
       var request = store.add(person);
       //var request = store.put(person, 2);
    
       request.onerror = function(e) {
           console.log("Error",e.target.error.name);
           //some type of error handler
       }
    
       request.onsuccess = function(e) {
          console.log("Woot! Did it.");
       }
    }
    
    //通过KEY查询记录
    function getPerson(e) {
        var key = document.querySelector("#key").value;
        if(key === "" || isNaN(key)) return;
    
        var transaction = db.transaction(["people"],"readonly");
        var store = transaction.objectStore("people");
    
        var request = store.get(Number(key));
    
        request.onsuccess = function(e) {
            var result = e.target.result;
            console.dir(result);
            if(result) {
               var s = "<p><h2>Key "+key+"</h2></p>";
               for(var field in result) {
                   s+= field+"="+result[field]+"<br/>";
               }
               document.querySelector("#status").innerHTML = s;
             } else {
                document.querySelector("#status").innerHTML = "<h2>No match!</h2>";
             }
         }
    }
    
    //获取所有记录
    function getPeople(e) {
    
        var s = "";
    
         db.transaction(["people"], "readonly").objectStore("people").openCursor().onsuccess = function(e) {
            var cursor = e.target.result;
            if(cursor) {
                s += "<p><h2>Key "+cursor.key+"</h2></p>";
                for(var field in cursor.value) {
                    s+= field+"="+cursor.value[field]+"<br/>";
                }
                s+="</p>";
                cursor.continue();
             }
             document.querySelector("#status2").innerHTML = s;
         }
    }
    
    //通过索引查询记录
    function getPeopleByNameIndex(e)
    {
        var name = document.querySelector("#name1").value;
    
        var transaction = db.transaction(["people"],"readonly");
        var store = transaction.objectStore("people");
        var index = store.index("name");
    
        //name is some value
        var request = index.get(name);
    
        request.onsuccess = function(e) {
           var result = e.target.result;
           if(result) {
               var s = "<p><h2>Name "+name+"</h2><p>";
               for(var field in result) {
                   s+= field+"="+result[field]+"<br/>";
               }
               s+="</p>";
        } else {
            document.querySelector("#status3").innerHTML = "<h2>No match!</h2>";
         }
       }
    }
    
    //通过索引查询记录
    function getPeopleByNameIndex1(e)
    {
        var s = "";
    
        var name = document.querySelector("#name1").value;
    
        var transaction = db.transaction(["people"],"readonly");
        var store = transaction.objectStore("people");
        var index = store.index("name");
    
        //name is some value
        index.openCursor().onsuccess = function(e) {
            var cursor = e.target.result;
            if(cursor) {
                s += "<p><h2>Key "+cursor.key+"</h2></p>";
                for(var field in cursor.value) {
                    s+= field+"="+cursor.value[field]+"<br/>";
                }
                s+="</p>";
                cursor.continue();
             }
             document.querySelector("#status3").innerHTML = s;
         }
    }
    
    </script>
    
    <p>添加数据<br/>
    <input type="text" id="name" placeholder="Name"><br/>
    <input type="email" id="email" placeholder="Email"><br/>
    <button id="addButton">Add Data</button>
    </p>
    
    <p>根据Key查询数据<br/>
    <input type="text" id="key" placeholder="Key"><br/>
    <button id="getButton">Get Data</button>
    </p>
    <div id="status" name="status"></div>
    
    <p>获取所有数据<br/>
    <button id="getAllButton">Get EveryOne</button>
    </p>
    <div id="status2" name="status2"></div>
    
    <p>根据索引:Name查询数据<br/>
        <input type="text" id="name1" placeholder="Name"><br/>
        <button id="getByName">Get ByName</button>
    </p>
    <div id="status3" name="status3"></div>
    

    将上面的代码复制到 indexed_db.html 中,用 Google Chrome 浏览器打开,就可以添加、查询数据。在 Chrome 的开发者工具中,能查看创建的 DB 、存储对象(可理解成表)以及表中添加的数据。

    【转】【前端类似雷竞技app】H5 类似雷竞技app机制浅析 - 移动端 Web 加载性能优化

    IndexedDB 有个非常强大的功能,就是 index(索引)。它可对 Value 对象中任何属性生成索引,然后可以基于索引进行 Value 对象的快速查询。

    要生成索引或支持索引查询数据,需求在首次生成存储对象时,调用接口生成属性的索引。可以同时对对象的多个不同属性创建索引。如下面代码就对name 和 email 两个属性都生成了索引。

    var objectStore = thisDB.createObjectStore("people",{ autoIncrement:true });
    //first arg is name of index, second is the path (col);
    objectStore.createIndex("name","name", {unique:false});
    objectStore.createIndex("email","email", {unique:true});
    

    生成索引后,就可以基于索引进行数据的查询。

    function getPeopleByNameIndex(e)
    {
    var name = document.querySelector("#name1").value;
    
    var transaction = db.transaction(["people"],"readonly");
    var store = transaction.objectStore("people");
    var index = store.index("name");
    
    //name is some value
    var request = index.get(name);
    request.onsuccess = function(e) {
        var result = e.target.result;
        if(result) {
            var s = "<p><h2>Name "+name+"</h2><p>";
            for(var field in result) {
                s+= field+"="+result[field]+"<br/>";
            }
            s+="</p>";
        } else {
            document.querySelector("#status3").innerHTML = "<h2>No match!</h2>";
        }
      }
    }
    

    分析:IndexedDB 是一种灵活且功能强大的数据存储机制,它集合了 Dom Storage 和 Web SQL Database 的优点,用于存储大块或复杂结构的数据,提供更大的存储空间,使用起来也比较简单。可以作为 Web SQL Database 的替代。不太适合静态文件的类似雷竞技app。

    1. 以key-value 的方式存取对象,可以是任何类型值或对象,包括二进制。
    2. 可以对对象任何属性生成索引,方便查询。
    3. 较大的存储空间,默认推荐250MB(分 HOST),比 Dom Storage 的5MB 要大的多。
    4. 通过数据库的事务(tranction)机制进行数据操作,保证数据一致性。
    5. 异步的 API 调用,避免造成等待而影响体验。

    Android 在4.4开始加入对 IndexedDB 的支持,只需打开允许 JS 执行的开关就好了。

    WebView myWebView = (WebView) findViewById(R.id.webview);
    WebSettings webSettings = myWebView.getSettings();
    webSettings.setJavaScriptEnabled(true);
    

    2.6 File System API

    File System API 是 H5 新加入的存储机制。它为 Web App 提供了一个虚拟的文件系统,就像 Native App 访问本地文件系统一样。由于安全性的考虑,这个虚拟文件系统有一定的限制。Web App 在虚拟的文件系统中,可以进行文件(夹)的创建、读、写、删除、遍历等操作。

    File System API 也是一种可选的类似雷竞技app机制,和前面的 SQLDatabase、IndexedDB 和 AppCache 等一样。File System API 有自己的一些特定的优势:

    1. 可以满足大块的二进制数据( large binary blobs)存储需求。
    2. 可以通过预加载资源文件来提高性能。
    3. 可以直接编辑文件。

    浏览器给虚拟文件系统提供了两种类型的存储空间:临时的和持久性的。临时的存储空间是由浏览器自动分配的,但可能被浏览器回收;持久性的存储空间需要显示的申请,申请时浏览器会给用户一提示,需要用户进行确认。持久性的存储空间是 WebApp 自己管理,浏览器不会回收,也不会清除内容。持久性的存储空间大小是通过配额来管理的,首次申请时会一个初始的配额,配额用完需要再次申请。

    虚拟的文件系统是运行在沙盒中。不同 WebApp 的虚拟文件系统是互相隔离的,虚拟文件系统与本地文件系统也是互相隔离的。

    File System API 提供了一组文件与文件夹的操作接口,有同步和异步两个版本,可满足不同的使用场景。下面通过一个文件创建、读、写的例子,演示下简单的功能与用法。

    <script type="text/javascript">
    
    window.requestFileSystem = window.requestFileSystem || window.webkitRequestFileSystem;
    
    //请求临时文件的存储空间
    if (window.requestFileSystem) {
         window.requestFileSystem(window.TEMPORARY, 5*1024*1024, initFS, errorHandler);
    }else{
      alert('Sorry! Your browser doesn\'t support the FileSystem API');
    }
    
    //请求成功回调
    function initFS(fs){
    
      //在根目录下打开log.txt文件,如果不存在就创建
      //fs就是成功返回的文件系统对象,fs.root代表根目录
      fs.root.getFile('log.txt', {create: true}, function(fileEntry) {
    
      //fileEntry是返回的一个文件对象,代表打开的文件
    
      //向文件写入指定内容
      writeFile(fileEntry);
    
      //将写入的内容又读出来,显示在页面上
      readFile(fileEntry);
    
      }, errorHandler);
    }
    
    //读取文件内容
    function readFile(fileEntry)
    {
        console.log('readFile');
    
       // Get a File object representing the file,
       // then use FileReader to read its contents.
       fileEntry.file(function(file) {
    
         console.log('createReader');
    
          var reader = new FileReader();
    
          reader.onloadend = function(e) {
    
            console.log('onloadend');
    
            var txtArea = document.createElement('textarea');
            txtArea.value = this.result;
            document.body.appendChild(txtArea);
          };
    
          reader.readAsText(file);
       }, errorHandler);
    }
    
    //向文件写入指定内容
    function writeFile(fileEntry)
    {
        console.log('writeFile');
    
        // Create a FileWriter object for our FileEntry (log.txt).
        fileEntry.createWriter(function(fileWriter) {
    
          console.log('createWriter');
    
          fileWriter.onwriteend = function(e) {
            console.log('Write completed');
          };
    
            fileWriter.onerror = function(e) {
              console.log('Write failed: ' + e.toString());
            };
    
            // Create a new Blob and write it to log.txt.
            var blob = new Blob(['Hello, World!'], {type: 'text/plain'});
    
            fileWriter.write(blob);
    
         }, errorHandler);
    }
    
    function errorHandler(err){
     var msg = 'An error occured: ' + err;
     console.log(msg);
    };
    
     </script>
    

    将上面代码复制到 file_system_api.html 文件中,用 Google Chrome 浏览器打开(现在 File System API 只有 Chrome 43+、Opera 32+ 以及 Chrome for Android 46+ 这三个浏览器支持)。由于 Google Chrome 禁用了本地 HTML 文件中的 File System API功能,在启动 Chrome 时,要加上”—allow-file-access-from-files“命令行参数。

    【转】【前端类似雷竞技app】H5 类似雷竞技app机制浅析 - 移动端 Web 加载性能优化

    上面截图,左边是 HTML 运行的结果,右边是 Chrome 开发者工具中看到的 Web 的文件系统。基本上 H5的几种类似雷竞技app机制的数据都能在这个开发者工具看到,非常方便。

    分析:File System API 给 Web App 带来了文件系统的功能,Native 文件系统的功能在 Web App 中都有相应的实现。任何需要通过文件来管理数据,或通过文件系统进行数据管理的场景都比较适合。

    到目前,Android 系统的 Webview 还不支持 File System API。

    3 移动端 Web 加载性能(类似雷竞技app)优化

    分析完 H5提供的各种类似雷竞技app机制,回到移动端(针对 Android,可能也适用于 iOS)的场景。现在 Android App(包括手 Q 和 WX)大多嵌入了 Webview 的组件(系统 Webview 或 QQ 游览器的 X5组件),通过内嵌Webview 来加载一些H5的运营活动页面或资讯页。这样可充分发挥Web前端的优势:快速开发、发布,灵活上下线。但 Webview 也有一些不可忽视的问题,比较突出的就是加载相对较慢,会相对消耗较多流量。

    通过对一些 H5页面进行调试及抓包发现,每次加载一个 H5页面,都会有较多的请求。除了 HTML 主 URL 自身的请求外,HTML外部引用的 JS、CSS、字体文件、图片都是一个独立的 HTTP 请求,每一个请求都串行的(可能有连接复用)。这么多请求串起来,再加上浏览器解析、渲染的时间,Web 整体的加载时间变得较长;请求文件越多,消耗的流量也会越多。我们可综合使用上面说到几种类似雷竞技app机制,来帮助我们优化 Web 的加载性能。

    【转】【前端类似雷竞技app】H5 类似雷竞技app机制浅析 - 移动端 Web 加载性能优化

    结论:综合各种类似雷竞技app机制比较,对于静态文件,如 JS、CSS、字体、图片等,适合通过浏览器类似雷竞技app机制来进行类似雷竞技app,通过类似雷竞技app文件可大幅提升 Web 的加载速度,且节省流量。但也有一些不足:类似雷竞技app文件需要首次加载后才会产生;浏览器类似雷竞技app的存储空间有限,类似雷竞技app有被清除的可能;类似雷竞技app的文件没有校验。要解决这些不足,可以参考手 Q 的离线包,它有效的解决了这些不足。

    对于 Web 在本地或服务器获取的数据,可以通过 Dom Storage 和 IndexedDB 进行类似雷竞技app。也在一定程度上减少和 Server 的交互,提高加载速度,同时节省流量。

    当然 Web 的性能优化,还包括选择合适的图片大小,避免 JS 和 CSS 造成的阻塞等。这就需要 Web 前端的同事根据一些规范和一些调试工具进行优化了。

    ]]>
    <![CDATA[阿琛]]>https://noahzy.com/zhuan-qian-duan-huan-cun-bian-tai-de-jing-tai-zi-yuan-huan-cun-yu-geng-xin/5d7713fb7fa7c611a5d554d1Tue, 10 Sep 2019 03:31:11 GMT

    原文转自前端农民工

    这是一个非常有趣的 非主流前端领域,这个领域要探索的是如何用工程手段解决前端开发和部署优化的综合问题,入行到现在一直在学习和实践中。

    在我的印象中,facebook是这个领域的鼻祖,有兴趣、有梯子的同学可以去看看facebook的页面源代码,体会一下什么叫工程化。

    接下来,我想从原理展开讲述,多图,较长,希望能有耐心看完。

    一个简单的页面


    【转】【前端类似雷竞技app】变态的静态资源类似雷竞技app与更新

    让我们返璞归真,从原始的前端开发讲起。上图是一个“可爱”的index.html页面和它的样式文件a.css,用文本编辑器写代码,无需编译,本地预览,确认OK,丢到服务器,等待用户访问。前端就是这么简单,好好玩啊,门槛好低啊,分分钟学会有木有!

    【转】【前端类似雷竞技app】变态的静态资源类似雷竞技app与更新

    然后我们访问页面,看到效果,再查看一下网络请求,200!不错,太™完美了!那么,研发完成。。。。了么?
    等等,这还没完呢!对于大公司来说,那些变态的访问量和性能指标,将会让前端一点也不“好玩”。
    看看那个a.css的请求吧,如果每次用户访问页面都要加载,是不是很影响性能,很浪费带宽啊,我们希望最好这样:

    【转】【前端类似雷竞技app】变态的静态资源类似雷竞技app与更新

    利用304,让浏览器使用本地类似雷竞技app。但,这样也就够了吗?不成!304叫协商类似雷竞技app,这玩意还是要和服务器通信一次,我们的优化级别是变态级,所以必须彻底灭掉这个请求,变成这样:

    【转】【前端类似雷竞技app】变态的静态资源类似雷竞技app与更新

    强制浏览器使用本地类似雷竞技app(cache-control/expires),不要和服务器通信。好了,请求方面的优化已经达到变态级别,那问题来了:你都不让浏览器发资源请求了,这类似雷竞技app咋更新?

    很好,相信有人想到了办法:**通过更新页面中引用的资源路径,让浏览器主动放弃类似雷竞技app,加载新资源。**好像这样:

    【转】【前端类似雷竞技app】变态的静态资源类似雷竞技app与更新

    下次上线,把链接地址改成新的版本,就更新资源了不是。OK,问题解决了么?!当然没有!大公司的变态又来了,思考这种情况:

    【转】【前端类似雷竞技app】变态的静态资源类似雷竞技app与更新

    页面引用了3个css,而某次上线只改了其中的a.css,如果所有链接都更新版本,就会导致b.css,c.css的类似雷竞技app也失效,那岂不是又有浪费了?!

    重新开启变态模式,我们不难发现,要解决这种问题,必须让url的修改与文件内容关联,也就是说,只有文件内容变化,才会导致相应url的变更,从而实现文件级别的精确类似雷竞技app控制。

    什么东西与文件内容相关呢?我们会很自然的联想到利用 数据摘要要算法 对文件求摘要信息,摘要信息与文件内容一一对应,就有了一种可以精确到单个文件粒度的类似雷竞技app控制依据了。好了,我们把url改成带摘要信息的:

    【转】【前端类似雷竞技app】变态的静态资源类似雷竞技app与更新

    这回再有文件修改,就只更新那个文件对应的url了,想到这里貌似很完美了。你觉得这就够了么?大公司告诉你:图样图森破!

    唉~~~~,让我喘口气

    现代互联网企业,为了进一步提升网站性能,会把静态资源和动态网页分集群部署,静态资源会被部署到CDN节点上,网页中引用的资源也会变成对应的部署路径:

    【转】【前端类似雷竞技app】变态的静态资源类似雷竞技app与更新

    好了,当我要更新静态资源的时候,同时也会更新html中的引用吧,就好像这样:

    【转】【前端类似雷竞技app】变态的静态资源类似雷竞技app与更新

    这次发布,同时改了页面结构和样式,也更新了静态资源对应的url地址,现在要发布代码上线,亲爱的前端研发同学,你来告诉我,咱们是先上线页面,还是先上线静态资源?

    先部署页面,再部署资源:在二者部署的时间间隔内,如果有用户访问页面,就会在新的页面结构中加载旧的资源,并且把这个旧版本的资源当做新版本类似雷竞技app起来,其结果就是:用户访问到了一个样式错乱的页面,除非手动刷新,否则在资源类似雷竞技app过期之前,页面会一直执行错误。

    先部署资源,再部署页面:在部署时间间隔之内,有旧版本资源本地类似雷竞技app的用户访问网站,由于请求的页面是旧版本的,资源引用没有改变,浏览器将直接使用本地类似雷竞技app,这种情况下页面展现正常;但没有本地类似雷竞技app或者类似雷竞技app过期的用户访问网站,就会出现旧版本页面加载新版本资源的情况,导致页面执行错误,但当页面完成部署,这部分用户再次访问页面又会恢复正常了。
    好的,上面一坨分析想说的就是:先部署谁都不成!都会导致部署过程中发生页面错乱的问题。所以,访问量不大的项目,可以让研发同学苦逼一把,等到半夜偷偷上线,先上静态资源,再部署页面,看起来问题少一些。
    但是,大公司超变态,没有这样的“绝对低峰期”,只有“相对低峰期”。So,为了稳定的服务,还得继续追求极致啊!

    这个奇葩问题,起源于资源的 覆盖式发布,用 待发布资源 覆盖 已发布资源,就有这种问题。解决它也好办,就是实现 非覆盖式发布。

    【转】【前端类似雷竞技app】变态的静态资源类似雷竞技app与更新

    看上图,用文件的摘要信息来对资源文件进行重命名,把摘要信息放到资源文件发布路径中,这样,内容有修改的资源就变成了一个新的文件发布到线上,不会覆盖已有的资源文件。上线过程中,先全量部署静态资源,再灰度部署页面,整个问题就比较完美的解决了。

    所以,大公司的静态资源优化方案,基本上要实现这么几个东西:

    1. 配置超长时间的本地类似雷竞技app —— 节省带宽,提高性能
    1. 采用内容摘要作为类似雷竞技app更新依据 —— 精确的类似雷竞技app控制
    2. 静态资源CDN部署 —— 优化网络请求
    3. 更资源发布路径实现非覆盖式发布 —— 平滑升级

    全套做下来,就是相对比较完整的静态资源类似雷竞技app控制方案了,而且,还要注意的是,静态资源的类似雷竞技app控制要求在 前端所有静态资源加载的位置都要做这样的处理 。是的,所有!什么js、css自不必说,还要包括js、css文件中引用的资源路径,由于涉及到摘要信息,引用资源的摘要信息也会引起引用文件本身的内容改变,从而形成级联的摘要变化,大概示意图就是:

    【转】【前端类似雷竞技app】变态的静态资源类似雷竞技app与更新

    好了,目前我们快速的学习了一下前端工程中关于静态资源类似雷竞技app要面临的优化和部署问题,新的问题又来了:这™让工程师怎么写码啊!!!

    要解释优化与工程的结合处理思路,又会扯出一堆有关模块化开发、资源加载、请求合并、前端框架等等的工程问题,以上只是开了个头,解决方案才是精髓,但要说的太多太多,有空再慢慢展开吧。

    总之,前端性能优化绝逼是一个工程问题!

    以上不是我YY的,可以观察 百度 或者 facebook 的页面以及静态资源源代码,查看它们的资源引用路径处理,以及网络请中静态资源的类似雷竞技app控制部分。再次赞叹facebook的前端工程建设水平,跪舔了。

    建议前端工程师多多关注前端工程领域,也许有人会觉得自己的产品很小,不用这么变态,但很有可能说不定某天你就需要做出这样的改变了。而且,如果我们能把事情做得更极致,为什么不去做呢?

    另外,也不要觉得这些是运维或者后端工程师要解决的问题。如果由其他角色来解决,大家总是把自己不关心的问题丢给别人,那么前端工程师的开发过程将受到极大的限制,这种情况甚至在某些大公司都不少见!

    妈妈,我再也不玩前端了。。。。5555

    业界实践

    Assets Pipeline
    Rails中的Assets Pipeline完成了以上所说的优化细节,对整个静态资源的管理上的设计思考也是如此,了解rails的人也可以把此答案当做是对rails中assets pipeline设计原理的分析。

    rails通过把静态资源变成erb模板文件,然后加入<%= asset_path 'image.png' %>,上线前预编译完成处理,fis的实现思路跟这个几乎完全一样,但我们当初确实不知道有rails的这套方案存在。

    相关资料:

    英文版:http://guides.rubyonrails.org/asset_pipeline.html
    中文版:http://guides.ruby-china.org/asset_pipeline.html
    FIS的解决方案
    用 F.I.S 包装了一个小工具,完整实现整个回答所说的最佳部署方案,并提供了源码对照,可以感受一下项目源码和部署代码的对照。

    源码项目:https://github.com/fouber/static-resource-digest-project
    部署项目:https://github.com/fouber/static-resource-digest-project-release
    部署项目可以理解为线上发布后的结果,可以在部署项目里查看所有资源引用的md5化处理。

    这个示例也可以用于和assets pipeline做比较。fis没有assets的目录规范约束,而且可以以独立工具的方式组合各种前端开发语言(coffee、less、sass/scss、stylus、markdown、jade、ejs、handlebars等等你能想到的),并与其他后端开发语言结合。

    assets pipeline的设计思想值得独立成工具用于前端工程,fis就当做这样的一个选择吧。

    ]]>
    <![CDATA[阿琛]]>今天自己在Ubuntu下搭建了一个SVN服务器,方便以后可以远程管理代码
    特此记录了一下搭建的过程

    1. 通过apt-get安装subversion

    sudo apt-get install subversion
    如果安装出错(例如某个包找不到),请先更新本地仓库数据,更新完成后再执行上面的命令重新安装
    sudo apt-get update

    2.找个目录当做SVN的根目录(目录地址随意)

    在home目录下创建一个名为svn的文件夹(文件夹的名字随便起)
    mkdir /home/svn

    3.创建数据仓库(可以根据需要创建多个)

    此处我新建了一个叫ZYQ的仓库
    sudo svnadmin create /home/svn/ZYQ

    4.进入版本库查看生成的相关文件

    依次执行以下命令

    cd /home/svn/ZYQ  
    ls  
    

    会看到ZYQ目录下面自动生成了一些目录和文件
    conf db  format  hooks  locks  README.txt

    ]]>
    https://noahzy.com/svn-ji-lu-yi-xia-zai-ubuntuxia-de-svnfu-wu-qi-de-da-jian-guo-cheng/5d7710e57fa7c611a5d554c7Tue, 10 Sep 2019 02:57:20 GMT今天自己在Ubuntu下搭建了一个SVN服务器,方便以后可以远程管理代码
    特此记录了一下搭建的过程

    1. 通过apt-get安装subversion

    sudo apt-get install subversion
    如果安装出错(例如某个包找不到),请先更新本地仓库数据,更新完成后再执行上面的命令重新安装
    sudo apt-get update

    2.找个目录当做SVN的根目录(目录地址随意)

    在home目录下创建一个名为svn的文件夹(文件夹的名字随便起)
    mkdir /home/svn

    3.创建数据仓库(可以根据需要创建多个)

    此处我新建了一个叫ZYQ的仓库
    sudo svnadmin create /home/svn/ZYQ

    4.进入版本库查看生成的相关文件

    依次执行以下命令

    cd /home/svn/ZYQ  
    ls  
    

    会看到ZYQ目录下面自动生成了一些目录和文件
    conf db  format  hooks  locks  README.txt

    我们主要关心的是conf和db文件
    conf文件夹下是存放主配置文件和用户、权限位置,db文件夹是存放svn转储后的数据。

    进入conf文件夹下面
    cd conf

    conf文件夹中有三个文件,分别是
    authz 是设置用户权限文件
    passwd 是存储用户及密码文件
    svnserve.conf 是主配置文件

    5.配置版本库

    编辑svnserve.conf
    vi svnserve.conf

      将以下参数去掉注释  
      #匿名用户访问权限,默认read,none为不允许匿名用户访问
      anon-access = none 
    
      #认证用户权限 可写    
      auth-access = write  
    
      #密码文件为passwd(默认在版本库/conf下面,也可以绝对路径指定文件位置)
      password-db = passwd 
    
      #权限文件为authz (默认在版本库/conf下面,也可以绝对路径指定文件位置)  
      authz-db = authz 
    

    编辑passwd文件 设定用户名和密码
    vi passwd

      #前面是用户名,后面是密码  
      [users]  
      user1 = 123456  
      user2 = 123456  
      user3 = 123456
    

    ==编辑authz文件 制定用户组 以及用户组下面的成员和权限 ==
    vi authz

      [groups]  
      #定义用户组,以及用户组下面的成员  
      manager = user1 
      guest = user2,user3  
    
      #manager用户组下面的成员对以根目录起始的ZYQ版本库具有读写权限  
      [ZYQ:/]
      @manager = rw  
    
      #guest用户组下面的成员对ZYQ版本库下media目录只读权限  
      [ZYQ:/media]     
      @guest = r
    

    6.启动SVN网络服务

    sudo svnserve -d -r /home/svn
    其中 -d 参数让 svnserve 运行在后台,-r表示代码仓库的根目录,至此一个最简单的SVN服务就搭建好了。
    

    sudo ps aux | grep svnserve 如果启动成功了 可以看到进程
    sudo netstat -antp |grep svnserve 查看端口是否启动成功
    sudo pkill svnserve 关闭SVN服务

    ]]>
    <![CDATA[阿琛]]>https://noahzy.com/ios-zhong-ming-ming-xiang-mu-ming-cheng-xu-yao-zhu-yi-de-shi-qing/5d761f0a7fa7c611a5d5548bTue, 10 Sep 2019 02:36:15 GMT今天要把某个工程的名称给重命名一下,折腾了一下午。
    特此记录了一下重命名的步骤和流程 ,以备以后不时之需。


    修改之前切记

    一定要将原始工程备份!!!
    一定要将原始工程备份!!!!
    一定要将原始工程备份!!!!!

    一、第一步:修改Project的名称

    先照着下面的网址步骤 把project的名称改了

    iOS项目的Project重命名方法图文教程

    iOS项目的Project重命名方法图文教程

    注意:如果项目没有使用CocoaPods来管理,恭喜你,你可以直接略过以下步骤了


    二、第二步:修改workspace的名称(CocoaPods)

    按照上面的教程做完之后再 pod install 一下  把原来的xxx.xcworkspace文件删了,因为 pod install后会新生成一个用新名字命名的.xcworkspace文件.
    ==如果跑起来了,那么恭喜你,你已经改名成功了。 ==
    如果还是跑不起来 (一般都是遇到这种第三方库自己有.a文件的情况)  一般报的是找不到xx类库  例如 library not found for -xxxx

    a.先将报错的第三方从Podfile里面先给删掉  然后重新pod install  
      接着运行之后会有很多原来这个第三方库的引用错误 。
      我们先把代码里面的相关类给注释掉 再次运行。
    

    注意:如果需要注释的地方实在是太多,不方便注释,可以直接走步骤三。
    如果你觉得步骤三麻烦(不想移除CocoaPods), 那么也可以把Podfile文件中所有的第三方删掉,然后pod install一次,等成功后,再把那些删掉的第三方加回Podfile文件中,再次pod install

    b.如果仍然报错,这时候应该是另一个第三方出错,重复a步骤。 (如果还是同一个第三方报错)
    
    c.如果弄了很久还是不行,往下看。
    

    三、移除CocoaPods

    把CocoaPods 从项目中移除 ,然后再重新创建CocoaPods

    (英文)如何把CocoaPods从项目中移除

    (中文)如何把CocoaPods从项目中移除

    注意:记得把Podfile备份一下哦,待会还要把CocoaPods重新加回去的

    四、

    如果你没走步骤三:那就把原来删掉的第三方加回去,再重新pod install一次

    如果移除了CocoaPods:把之前备份的Podfile放回去,再重新pod install一次

    ]]>
    <![CDATA[阿琛]]>一名iOS Developer.  坐标深圳

    多看、多听、多想

  • 联系我
  • QQ: 494564648
  • Email: 494564648@qq.com
  • ]]>
    https://noahzy.com/guan-yu-wo/5d7867237fa7c611a5d55636Tue, 10 Sep 2019 02:03:00 GMT一名iOS Developer.  坐标深圳

    多看、多听、多想

  • 联系我
  • QQ: 494564648
  • Email: 494564648@qq.com
  • ]]>