libinfs

libinfs 查看完整档案

杭州编辑  |  填写毕业院校  |  填写所在公司/组织 www.cnblogs.com/libinfs/ 编辑
编辑

我希望在 segmentfault 做一个严肃认真的专栏作者,如果我的文章对你有帮助,请点击文章下方“赞”或“赞赏支持”给我支持,十分感谢。

个人动态

libinfs 收藏了文章 · 2019-08-14

服务端高并发分布式架构演进之路

1. 概述

本文以淘宝作为例子,介绍从一百个到千万级并发情况下服务端的架构的演进过程,同时列举出每个演进阶段会遇到的相关技术,让大家对架构的演进有一个整体的认知,文章最后汇总了一些架构设计的原则。

特别说明:本文以淘宝为例仅仅是为了便于说明演进过程可能遇到的问题,并非是淘宝真正的技术演进路径

2. 基本概念

在介绍架构之前,为了避免部分读者对架构设计中的一些概念不了解,下面对几个最基础的概念进行介绍:

  • 分布式
    系统中的多个模块在不同服务器上部署,即可称为分布式系统,如Tomcat和数据库分别部署在不同的服务器上,或两个相同功能的Tomcat分别部署在不同服务器上
  • 高可用
    系统中部分节点失效时,其他节点能够接替它继续提供服务,则可认为系统具有高可用性
  • 集群
    一个特定领域的软件部署在多台服务器上并作为一个整体提供一类服务,这个整体称为集群。如Zookeeper中的Master和Slave分别部署在多台服务器上,共同组成一个整体提供集中配置服务。在常见的集群中,客户端往往能够连接任意一个节点获得服务,并且当集群中一个节点掉线时,其他节点往往能够自动的接替它继续提供服务,这时候说明集群具有高可用性
  • 负载均衡
    请求发送到系统时,通过某些方式把请求均匀分发到多个节点上,使系统中每个节点能够均匀的处理请求负载,则可认为系统是负载均衡的
  • 正向代理和反向代理
    系统内部要访问外部网络时,统一通过一个代理服务器把请求转发出去,在外部网络看来就是代理服务器发起的访问,此时代理服务器实现的是正向代理;当外部请求进入系统时,代理服务器把该请求转发到系统中的某台服务器上,对外部请求来说,与之交互的只有代理服务器,此时代理服务器实现的是反向代理。简单来说,正向代理是代理服务器代替系统内部来访问外部网络的过程,反向代理是外部请求访问系统时通过代理服务器转发到内部服务器的过程。

3. 架构演进

3.1 单机架构

clipboard.png

以淘宝作为例子。在网站最初时,应用数量与用户数都较少,可以把Tomcat和数据库部署在同一台服务器上。浏览器往www.taobao.com发起请求时,首先经过DNS服务器(域名系统)把域名转换为实际IP地址10.102.4.1,浏览器转而访问该IP对应的Tomcat。

随着用户数的增长,Tomcat和数据库之间竞争资源,单机性能不足以支撑业务

3.2 第一次演进:Tomcat与数据库分开部署

clipboard.png

Tomcat和数据库分别独占服务器资源,显著提高两者各自性能。

随着用户数的增长,并发读写数据库成为瓶颈

3.3 第二次演进:引入本地缓存和分布式缓存

clipboard.png

在Tomcat同服务器上或同JVM中增加本地缓存,并在外部增加分布式缓存,缓存热门商品信息或热门商品的html页面等。通过缓存能把绝大多数请求在读写数据库前拦截掉,大大降低数据库压力。其中涉及的技术包括:使用memcached作为本地缓存,使用Redis作为分布式缓存,还会涉及缓存一致性、缓存穿透/击穿、缓存雪崩、热点数据集中失效等问题。

缓存抗住了大部分的访问请求,随着用户数的增长,并发压力主要落在单机的Tomcat上,响应逐渐变慢

3.4 第三次演进:引入反向代理实现负载均衡

clipboard.png

在多台服务器上分别部署Tomcat,使用反向代理软件(Nginx)把请求均匀分发到每个Tomcat中。此处假设Tomcat最多支持100个并发,Nginx最多支持50000个并发,那么理论上Nginx把请求分发到500个Tomcat上,就能抗住50000个并发。其中涉及的技术包括:Nginx、HAProxy,两者都是工作在网络第七层的反向代理软件,主要支持http协议,还会涉及session共享、文件上传下载的问题。

反向代理使应用服务器可支持的并发量大大增加,但并发量的增长也意味着更多请求穿透到数据库,单机的数据库最终成为瓶颈

3.5 第四次演进:数据库读写分离

clipboard.png

把数据库划分为读库和写库,读库可以有多个,通过同步机制把写库的数据同步到读库,对于需要查询最新写入数据场景,可通过在缓存中多写一份,通过缓存获得最新数据。其中涉及的技术包括:Mycat,它是数据库中间件,可通过它来组织数据库的分离读写和分库分表,客户端通过它来访问下层数据库,还会涉及数据同步,数据一致性的问题。

业务逐渐变多,不同业务之间的访问量差距较大,不同业务直接竞争数据库,相互影响性能

3.6 第五次演进:数据库按业务分库

clipboard.png

把不同业务的数据保存到不同的数据库中,使业务之间的资源竞争降低,对于访问量大的业务,可以部署更多的服务器来支撑。这样同时导致跨业务的表无法直接做关联分析,需要通过其他途径来解决,但这不是本文讨论的重点,有兴趣的可以自行搜索解决方案。

随着用户数的增长,单机的写库会逐渐会达到性能瓶颈

3.7 第六次演进:把大表拆分为小表

clipboard.png

比如针对评论数据,可按照商品ID进行hash,路由到对应的表中存储;针对支付记录,可按照小时创建表,每个小时表继续拆分为小表,使用用户ID或记录编号来路由数据。只要实时操作的表数据量足够小,请求能够足够均匀的分发到多台服务器上的小表,那数据库就能通过水平扩展的方式来提高性能。其中前面提到的Mycat也支持在大表拆分为小表情况下的访问控制。

这种做法显著的增加了数据库运维的难度,对DBA的要求较高。数据库设计到这种结构时,已经可以称为分布式数据库,但是这只是一个逻辑的数据库整体,数据库里不同的组成部分是由不同的组件单独来实现的,如分库分表的管理和请求分发,由Mycat实现,SQL的解析由单机的数据库实现,读写分离可能由网关和消息队列来实现,查询结果的汇总可能由数据库接口层来实现等等,这种架构其实是MPP(大规模并行处理)架构的一类实现。

目前开源和商用都已经有不少MPP数据库,开源中比较流行的有Greenplum、TiDB、Postgresql XC、HAWQ等,商用的如南大通用的GBase、睿帆科技的雪球DB、华为的LibrA等等,不同的MPP数据库的侧重点也不一样,如TiDB更侧重于分布式OLTP场景,Greenplum更侧重于分布式OLAP场景,这些MPP数据库基本都提供了类似Postgresql、Oracle、MySQL那样的SQL标准支持能力,能把一个查询解析为分布式的执行计划分发到每台机器上并行执行,最终由数据库本身汇总数据进行返回,也提供了诸如权限管理、分库分表、事务、数据副本等能力,并且大多能够支持100个节点以上的集群,大大降低了数据库运维的成本,并且使数据库也能够实现水平扩展。

数据库和Tomcat都能够水平扩展,可支撑的并发大幅提高,随着用户数的增长,最终单机的Nginx会成为瓶颈

3.8 第七次演进:使用LVS或F5来使多个Nginx负载均衡

clipboard.png

由于瓶颈在Nginx,因此无法通过两层的Nginx来实现多个Nginx的负载均衡。图中的LVS和F5是工作在网络第四层的负载均衡解决方案,其中LVS是软件,运行在操作系统内核态,可对TCP请求或更高层级的网络协议进行转发,因此支持的协议更丰富,并且性能也远高于Nginx,可假设单机的LVS可支持几十万个并发的请求转发;F5是一种负载均衡硬件,与LVS提供的能力类似,性能比LVS更高,但价格昂贵。由于LVS是单机版的软件,若LVS所在服务器宕机则会导致整个后端系统都无法访问,因此需要有备用节点。可使用keepalived软件模拟出虚拟IP,然后把虚拟IP绑定到多台LVS服务器上,浏览器访问虚拟IP时,会被路由器重定向到真实的LVS服务器,当主LVS服务器宕机时,keepalived软件会自动更新路由器中的路由表,把虚拟IP重定向到另外一台正常的LVS服务器,从而达到LVS服务器高可用的效果。

此处需要注意的是,上图中从Nginx层到Tomcat层这样画并不代表全部Nginx都转发请求到全部的Tomcat,在实际使用时,可能会是几个Nginx下面接一部分的Tomcat,这些Nginx之间通过keepalived实现高可用,其他的Nginx接另外的Tomcat,这样可接入的Tomcat数量就能成倍的增加。

由于LVS也是单机的,随着并发数增长到几十万时,LVS服务器最终会达到瓶颈,此时用户数达到千万甚至上亿级别,用户分布在不同的地区,与服务器机房距离不同,导致了访问的延迟会明显不同

3.9 第八次演进:通过DNS轮询实现机房间的负载均衡

clipboard.png

在DNS服务器中可配置一个域名对应多个IP地址,每个IP地址对应到不同的机房里的虚拟IP。当用户访问www.taobao.com时,DNS服务器会使用轮询策略或其他策略,来选择某个IP供用户访问。此方式能实现机房间的负载均衡,至此,系统可做到机房级别的水平扩展,千万级到亿级的并发量都可通过增加机房来解决,系统入口处的请求并发量不再是问题。

随着数据的丰富程度和业务的发展,检索、分析等需求越来越丰富,单单依靠数据库无法解决如此丰富的需求

3.10 第九次演进:引入NoSQL数据库和搜索引擎等技术

clipboard.png

当数据库中的数据多到一定规模时,数据库就不适用于复杂的查询了,往往只能满足普通查询的场景。对于统计报表场景,在数据量大时不一定能跑出结果,而且在跑复杂查询时会导致其他查询变慢,对于全文检索、可变数据结构等场景,数据库天生不适用。因此需要针对特定的场景,引入合适的解决方案。如对于海量文件存储,可通过分布式文件系统HDFS解决,对于key value类型的数据,可通过HBase和Redis等方案解决,对于全文检索场景,可通过搜索引擎如ElasticSearch解决,对于多维分析场景,可通过Kylin或Druid等方案解决。

当然,引入更多组件同时会提高系统的复杂度,不同的组件保存的数据需要同步,需要考虑一致性的问题,需要有更多的运维手段来管理这些组件等。

引入更多组件解决了丰富的需求,业务维度能够极大扩充,随之而来的是一个应用中包含了太多的业务代码,业务的升级迭代变得困难

3.11 第十次演进:大应用拆分为小应用

clipboard.png

按照业务板块来划分应用代码,使单个应用的职责更清晰,相互之间可以做到独立升级迭代。这时候应用之间可能会涉及到一些公共配置,可以通过分布式配置中心Zookeeper来解决。

不同应用之间存在共用的模块,由应用单独管理会导致相同代码存在多份,导致公共功能升级时全部应用代码都要跟着升级

3.12 第十一次演进:复用的功能抽离成微服务

clipboard.png

如用户管理、订单、支付、鉴权等功能在多个应用中都存在,那么可以把这些功能的代码单独抽取出来形成一个单独的服务来管理,这样的服务就是所谓的微服务,应用和服务之间通过HTTP、TCP或RPC请求等多种方式来访问公共服务,每个单独的服务都可以由单独的团队来管理。此外,可以通过Dubbo、SpringCloud等框架实现服务治理、限流、熔断、降级等功能,提高服务的稳定性和可用性。

不同服务的接口访问方式不同,应用代码需要适配多种访问方式才能使用服务,此外,应用访问服务,服务之间也可能相互访问,调用链将会变得非常复杂,逻辑变得混乱

3.13 第十二次演进:引入企业服务总线ESB屏蔽服务接口的访问差异

clipboard.png

通过ESB统一进行访问协议转换,应用统一通过ESB来访问后端服务,服务与服务之间也通过ESB来相互调用,以此降低系统的耦合程度。这种单个应用拆分为多个应用,公共服务单独抽取出来来管理,并使用企业消息总线来解除服务之间耦合问题的架构,就是所谓的SOA(面向服务)架构,这种架构与微服务架构容易混淆,因为表现形式十分相似。个人理解,微服务架构更多是指把系统里的公共服务抽取出来单独运维管理的思想,而SOA架构则是指一种拆分服务并使服务接口访问变得统一的架构思想,SOA架构中包含了微服务的思想。

业务不断发展,应用和服务都会不断变多,应用和服务的部署变得复杂,同一台服务器上部署多个服务还要解决运行环境冲突的问题,此外,对于如大促这类需要动态扩缩容的场景,需要水平扩展服务的性能,就需要在新增的服务上准备运行环境,部署服务等,运维将变得十分困难

3.14 第十三次演进:引入容器化技术实现运行环境隔离与动态服务管理

clipboard.png

目前最流行的容器化技术是Docker,最流行的容器管理服务是Kubernetes(K8S),应用/服务可以打包为Docker镜像,通过K8S来动态分发和部署镜像。Docker镜像可理解为一个能运行你的应用/服务的最小的操作系统,里面放着应用/服务的运行代码,运行环境根据实际的需要设置好。把整个“操作系统”打包为一个镜像后,就可以分发到需要部署相关服务的机器上,直接启动Docker镜像就可以把服务起起来,使服务的部署和运维变得简单。

在大促的之前,可以在现有的机器集群上划分出服务器来启动Docker镜像,增强服务的性能,大促过后就可以关闭镜像,对机器上的其他服务不造成影响(在3.14节之前,服务运行在新增机器上需要修改系统配置来适配服务,这会导致机器上其他服务需要的运行环境被破坏)。

使用容器化技术后服务动态扩缩容问题得以解决,但是机器还是需要公司自身来管理,在非大促的时候,还是需要闲置着大量的机器资源来应对大促,机器自身成本和运维成本都极高,资源利用率低

3.15 第十四次演进:以云平台承载系统

clipboard.png

系统可部署到公有云上,利用公有云的海量机器资源,解决动态硬件资源的问题,在大促的时间段里,在云平台中临时申请更多的资源,结合Docker和K8S来快速部署服务,在大促结束后释放资源,真正做到按需付费,资源利用率大大提高,同时大大降低了运维成本。

所谓的云平台,就是把海量机器资源,通过统一的资源管理,抽象为一个资源整体,在之上可按需动态申请硬件资源(如CPU、内存、网络等),并且之上提供通用的操作系统,提供常用的技术组件(如Hadoop技术栈,MPP数据库等)供用户使用,甚至提供开发好的应用,用户不需要关系应用内部使用了什么技术,就能够解决需求(如音视频转码服务、邮件服务、个人博客等)。在云平台中会涉及如下几个概念:

  • IaaS:基础设施即服务。对应于上面所说的机器资源统一为资源整体,可动态申请硬件资源的层面;
  • PaaS:平台即服务。对应于上面所说的提供常用的技术组件方便系统的开发和维护;
  • SaaS:软件即服务。对应于上面所说的提供开发好的应用或服务,按功能或性能要求付费。
至此,以上所提到的从高并发访问问题,到服务的架构和系统实施的层面都有了各自的解决方案,但同时也应该意识到,在上面的介绍中,其实是有意忽略了诸如跨机房数据同步、分布式事务实现等等的实际问题,这些问题以后有机会再拿出来单独讨论

4. 架构设计总结

  • 架构的调整是否必须按照上述演变路径进行?
    不是的,以上所说的架构演变顺序只是针对某个侧面进行单独的改进,在实际场景中,可能同一时间会有几个问题需要解决,或者可能先达到瓶颈的是另外的方面,这时候就应该按照实际问题实际解决。如在政府类的并发量可能不大,但业务可能很丰富的场景,高并发就不是重点解决的问题,此时优先需要的可能会是丰富需求的解决方案。
  • 对于将要实施的系统,架构应该设计到什么程度?
    对于单次实施并且性能指标明确的系统,架构设计到能够支持系统的性能指标要求就足够了,但要留有扩展架构的接口以便不备之需。对于不断发展的系统,如电商平台,应设计到能满足下一阶段用户量和性能指标要求的程度,并根据业务的增长不断的迭代升级架构,以支持更高的并发和更丰富的业务。
  • 服务端架构和大数据架构有什么区别?
    所谓的“大数据”其实是海量数据采集清洗转换、数据存储、数据分析、数据服务等场景解决方案的一个统称,在每一个场景都包含了多种可选的技术,如数据采集有Flume、Sqoop、Kettle等,数据存储有分布式文件系统HDFS、FastDFS,NoSQL数据库HBase、MongoDB等,数据分析有Spark技术栈、机器学习算法等。总的来说大数据架构就是根据业务的需求,整合各种大数据组件组合而成的架构,一般会提供分布式存储、分布式计算、多维分析、数据仓库、机器学习算法等能力。而服务端架构更多指的是应用组织层面的架构,底层能力往往是由大数据架构来提供。
  • 有没有一些架构设计的原则?

    • N+1设计。系统中的每个组件都应做到没有单点故障;
    • 回滚设计。确保系统可以向前兼容,在系统升级时应能有办法回滚版本;
    • 禁用设计。应该提供控制具体功能是否可用的配置,在系统出现故障时能够快速下线功能;
    • 监控设计。在设计阶段就要考虑监控的手段;
    • 多活数据中心设计。若系统需要极高的高可用,应考虑在多地实施数据中心进行多活,至少在一个机房断电的情况下系统依然可用;
    • 采用成熟的技术。刚开发的或开源的技术往往存在很多隐藏的bug,出了问题没有商业支持可能会是一个灾难;
    • 资源隔离设计。应避免单一业务占用全部资源;
    • 架构应能水平扩展。系统只有做到能水平扩展,才能有效避免瓶颈问题;
    • 非核心则购买。非核心功能若需要占用大量的研发资源才能解决,则考虑购买成熟的产品;
    • 使用商用硬件。商用硬件能有效降低硬件故障的机率;
    • 快速迭代。系统应该快速开发小功能模块,尽快上线进行验证,早日发现问题大大降低系统交付的风险;
    • 无状态设计。服务接口应该做成无状态的,当前接口的访问不依赖于接口上次访问的状态。
查看原文

libinfs 赞了文章 · 2018-12-07

很全很全的前端本地存储讲解

最近一直在搞基础的东西,弄了一个持续更新的github笔记,可以去看看,诚意之作(本来就是写给自己看的……)链接地址:Front-End-Basics

此篇文章的地址:三种本地存储方式

基础笔记的github地址:https://github.com/qiqihaobenben/Front-End-Basics ,可以watch,也可以star。

发完之后,就有同学表示,你这也不全呀,还有评论说:吹牛不交税……,应该是被人举报了,现在看不到那条评论了,但是我邮箱里面有哦……本人水平有限只用过那三种,不过人家说的也是事实,我就有两个想法,第一是把标题改为“不太全的前端本地存储讲解”,第二种是把那不全的尽力补一下,嗯,做对的事情,我选择了第二种,补充的东西在最后。

正文开始……


三种本地存储方式

cookie

前言

网络早期最大的问题之一是如何管理状态。简而言之,服务器无法知道两个请求是否来自同一个浏览器。当时最简单的方法是在请求时,在页面中插入一些参数,并在下一个请求中传回参数。这需要使用包含参数的隐藏的表单,或者作为URL参数的一部分传递。这两个解决方案都手动操作,容易出错。cookie出现来解决这个问题。

作用

cookie是纯文本,没有可执行代码。存储数据,当用户访问了某个网站(网页)的时候,我们就可以通过cookie来向访问者电脑上存储数据,或者某些网站为了辨别用户身份、进行session跟踪而储存在用户本地终端上的数据(通常经过加密)

如何工作

当网页要发http请求时,浏览器会先检查是否有相应的cookie,有则自动添加在request header中的cookie字段中。这些是浏览器自动帮我们做的,而且每一次http请求浏览器都会自动帮我们做。这个特点很重要,因为这关系到“什么样的数据适合存储在cookie中”。

存储在cookie中的数据,每次都会被浏览器自动放在http请求中,如果这些数据并不是每个请求都需要发给服务端的数据,浏览器这设置自动处理无疑增加了网络开销;但如果这些数据是每个请求都需要发给服务端的数据(比如身份认证信息),浏览器这设置自动处理就大大免去了重复添加操作。所以对于那种设置“每次请求都要携带的信息(最典型的就是身份认证信息)”就特别适合放在cookie中,其他类型的数据就不适合了。

特征

  1. 不同的浏览器存放的cookie位置不一样,也是不能通用的。
  2. cookie的存储是以域名形式进行区分的,不同的域下存储的cookie是独立的。
  3. 我们可以设置cookie生效的域(当前设置cookie所在域的子域),也就是说,我们能够操作的cookie是当前域以及当前域下的所有子域
  4. 一个域名下存放的cookie的个数是有限制的,不同的浏览器存放的个数不一样,一般为20个。
  5. 每个cookie存放的内容大小也是有限制的,不同的浏览器存放大小不一样,一般为4KB。
  6. cookie也可以设置过期的时间,默认是会话结束的时候,当时间到期自动销毁

cookie值既可以设置,也可以读取。

设置

客户端设置

document.cookie = '名字=值';
document.cookie = 'username=cfangxu;domain=baike.baidu.com'    并且设置了生效域

注意: 客户端可以设置cookie 的下列选项:expires、domain、path、secure(有条件:只有在https协议的网页中,客户端设置secure类型的 cookie 才能成功),但无法设置HttpOnly选项。

服务器端设置
不管你是请求一个资源文件(如 html/js/css/图片),还是发送一个ajax请求,服务端都会返回response。而response header中有一项叫set-cookie,是服务端专门用来设置cookie的。

Set-Cookie 消息头是一个字符串,其格式如下(中括号中的部分是可选的):
Set-Cookie: value[; expires=date][; domain=domain][; path=path][; secure]

注意: 一个set-Cookie字段只能设置一个cookie,当你要想设置多个 cookie,需要添加同样多的set-Cookie字段。
服务端可以设置cookie 的所有选项:expires、domain、path、secure、HttpOnly
通过 Set-Cookie 指定的这些可选项只会在浏览器端使用,而不会被发送至服务器端。

读取

我们通过document.cookie来获取当前网站下的cookie的时候,得到的字符串形式的值,它包含了当前网站下所有的cookie(为避免跨域脚本(xss)攻击,这个方法只能获取非 HttpOnly 类型的cookie)。它会把所有的cookie通过一个分号+空格的形式串联起来,例如username=chenfangxu; job=coding

修改 cookie

要想修改一个cookie,只需要重新赋值就行,旧的值会被新的值覆盖。但要注意一点,在设置新cookie时,path/domain这几个选项一定要旧cookie 保持一样。否则不会修改旧值,而是添加了一个新的 cookie。

删除

把要删除的cookie的过期时间设置成已过去的时间,path/domain/这几个选项一定要旧cookie 保持一样。

注意

如果只设置一个值,那么算cookie中的value; 设置的两个cookie,key值如果设置的相同,下面的也会把上面的覆盖。

cookie的属性(可选项)

过期时间

如果我们想长时间存放一个cookie。需要在设置这个cookie的时候同时给他设置一个过期的时间。如果不设置,cookie默认是临时存储的,当浏览器关闭进程的时候自动销毁

注意:document.cookie = '名称=值;expires=' + GMT(格林威治时间)格式的日期型字符串; 

一般设置天数:new Date().setDate( oDate.getDate() + 5 ); 比当前时间多5天

一个设置cookie时效性的例子

function setCookie(c_name, value, expiredays){
    var exdate=new Date();
    exdate.setDate(exdate.getDate() + expiredays);
    document.cookie=c_name+ "=" + escape(value) + ((expiredays==null) ? "" : ";expires="+exdate.toGMTString())
}
使用方法:setCookie('username','cfangxu',30)
expires 是 http/1.0协议中的选项,在新的http/1.1协议中expires已经由 max-age 选项代替,两者的作用都是限制cookie 的有效时间。expires的值是一个时间点(cookie失效时刻= expires),而max-age 的值是一个以秒为单位时间段(cookie失效时刻= 创建时刻+ max-age)。
另外,max-age 的默认值是 -1(即有效期为 session );max-age有三种可能值:负数、0、正数。
负数:有效期session;
0:删除cookie;
正数:有效期为创建时刻+ max-age

cookie的域概念(domain选项)

domain指定了 cookie 将要被发送至哪个或哪些域中。默认情况下,domain 会被设置为创建该 cookie 的页面所在的域名,所以当给相同域名发送请求时该 cookie 会被发送至服务器。

浏览器会把 domain 的值与请求的域名做一个尾部比较(即从字符串的尾部开始比较),并将匹配的 cookie 发送至服务器。

客户端设置

document.cookie = "username=cfangxu;path=/;domain=qq.com"
如上:“www.qq.com" 与 "sports.qq.com" 公用一个关联的域名"qq.com",我们如果想让 "sports.qq.com" 下的cookie被 "www.qq.com" 访问,我们就需要用到 cookie 的domain属性,并且需要把path属性设置为 "/"。

服务端设置

Set-Cookie: username=cfangxu;path=/;domain=qq.com
注:一定的是同域之间的访问,不能把domain的值设置成非主域的域名。

cookie的路径概念(path选项)

cookie 一般都是由于用户访问页面而被创建的,可是并不是只有在创建 cookie 的页面才可以访问这个 cookie。
因为安全方面的考虑,默认情况下,只有与创建 cookie 的页面在同一个目录或子目录下的网页才可以访问。
即path属性可以为服务器特定文档指定cookie,这个属性设置的url且带有这个前缀的url路径都是有效的。

客户端设置

 最常用的例子就是让 cookie 在根目录下,这样不管是哪个子页面创建的 cookie,所有的页面都可以访问到了。

document.cookie = "username=cfangxu; path=/"

服务端设置

Set-Cookie:name=cfangxu; path=/blog

如上设置:path 选项值会与 /blog,/blogrool 等等相匹配;任何以 /blog 开头的选项都是合法的。需要注意的是,只有在 domain 选项核实完毕之后才会对 path 属性进行比较。path 属性的默认值是发送 Set-Cookie 消息头所对应的 URL 中的 path 部分。

domain和path总结:

domain是域名,path是路径,两者加起来就构成了 URL,domain和path一起来限制 cookie 能被哪些 URL 访问。
所以domain和path2个选项共同决定了cookie何时被浏览器自动添加到请求头部中发送出去。如果没有设置这两个选项,则会使用默认值。domain的默认值为设置该cookie的网页所在的域名,path默认值为设置该cookie的网页所在的目录。

cookie的安全性(secure选项)

通常 cookie 信息都是使用HTTP连接传递数据,这种传递方式很容易被查看,所以 cookie 存储的信息容易被窃取。假如 cookie 中所传递的内容比较重要,那么就要求使用加密的数据传输。

secure选项用来设置cookie只在确保安全的请求中才会发送。当请求是HTTPS或者其他安全协议时,包含 secure 选项的 cookie 才能被发送至服务器。

document.cookie = "username=cfangxu; secure"

把cookie设置为secure,只保证 cookie 与服务器之间的数据传输过程加密,而保存在本地的 cookie文件并不加密。就算设置了secure 属性也并不代表他人不能看到你机器本地保存的 cookie 信息。机密且敏感的信息绝不应该在 cookie 中存储或传输,因为 cookie 的整个机制原本都是不安全的

注意:如果想在客户端即网页中通过 js 去设置secure类型的 cookie,必须保证网页是https协议的。在http协议的网页中是无法设置secure类型cookie的。

httpOnly

这个选项用来设置cookie是否能通过 js 去访问。默认情况下,cookie不会带httpOnly选项(即为空),所以默认情况下,客户端是可以通过js代码去访问(包括读取、修改、删除等)这个cookie的。当cookie带httpOnly选项时,客户端则无法通过js代码去访问(包括读取、修改、删除等)这个cookie。

在客户端是不能通过js代码去设置一个httpOnly类型的cookie的,这种类型的cookie只能通过服务端来设置。

cookie的编码

cookie其实是个字符串,但这个字符串中等号、分号、空格被当做了特殊符号。所以当cookie的 key 和 value 中含有这3个特殊字符时,需要对其进行额外编码,一般会用escape进行编码,读取时用unescape进行解码;当然也可以用encodeURIComponent/decodeURIComponent或者encodeURI/decodeURI,查看关于编码的介绍

第三方cookie

通常cookie的域和浏览器地址的域匹配,这被称为第一方cookie。那么第三方cookie就是cookie的域和地址栏中的域不匹配,这种cookie通常被用在第三方广告网站。为了跟踪用户的浏览记录,并且根据收集的用户的浏览习惯,给用户推送相关的广告。
关于第三方cookie和cookie的安全问题可以查看https://mp.weixin.qq.com/s/oOGIuJCplPVW3BuIx9tNQg



localStorage(本地存储)

HTML5新方法,不过IE8及以上浏览器都兼容。

特点

  • 生命周期:持久化的本地存储,除非主动删除数据,否则数据是永远不会过期的。
  • 存储的信息在同一域中是共享的。
  • 当本页操作(新增、修改、删除)了localStorage的时候,本页面不会触发storage事件,但是别的页面会触发storage事件。
  • 大小:据说是5M(跟浏览器厂商有关系)
  • 在非IE下的浏览中可以本地打开。IE浏览器要在服务器中打开。
  • localStorage本质上是对字符串的读取,如果存储内容多的话会消耗内存空间,会导致页面变卡
  • localStorage受同源策略的限制

设置

localStorage.setItem('username','cfangxu');

获取

localStorage.getItem('username')
也可以获取键名
localStorage.key(0) #获取第一个键名

删除

localStorage.removeItem('username')
也可以一次性清除所有存储
localStorage.clear()

storage事件

当storage发生改变的时候触发。
注意: 当前页面对storage的操作会触发其他页面的storage事件
事件的回调函数中有一个参数event,是一个StorageEvent对象,提供了一些实用的属性,如下表:

PropertyTypeDescription
keyStringThe named key that was added, removed, or moddified
oldValueAnyThe previous value(now overwritten), or null if a new item was added
newValueAnyThe new value, or null if an item was added
url/uriStringThe page that called the method that triggered this change


sessionStorage

其实跟localStorage差不多,也是本地存储,会话本地存储

特点:

  • 用于本地存储一个会话(session)中的数据,这些数据只有在同一个会话中的页面才能访问并且当会话结束后数据也随之销毁。因此sessionStorage不是一种持久化的本地存储,仅仅是会话级别的存储。也就是说只要这个浏览器窗口没有关闭,即使刷新页面或进入同源另一页面,数据仍然存在。关闭窗口后,sessionStorage即被销毁,或者在新窗口打开同源的另一个页面,sessionStorage也是没有的。


cookie、localStorage、sessionStorage区别

  • 相同:在本地(浏览器端)存储数据
  • 不同:

    localStorage、sessionStorage

    localStorage只要在相同的协议、相同的主机名、相同的端口下,就能读取/修改到同一份localStorage数据。

    sessionStorage比localStorage更严苛一点,除了协议、主机名、端口外,还要求在同一窗口(也就是浏览器的标签页)下。

    localStorage是永久存储,除非手动删除。

    sessionStorage当会话结束(当前页面关闭的时候,自动销毁)

    cookie的数据会在每一次发送http请求的时候,同时发送给服务器而localStorage、sessionStorage不会。


扩展其他的前端存储方式(不常用)

web SQL database

先说个会被取代的,为什么会被取代,主要有以下几个原因:

  1. W3C舍弃 Web SQL database草案,而且是在2010年年底,规范不支持了,浏览器厂商已经支持的就支持了,没有支持的也不打算支持了,比如IE和Firefox。
  2. 为什么要舍弃?因为 Web SQL database 本质上是一个关系型数据库,后端可能熟悉,但是前端就有很多不熟悉了,虽然SQL的简单操作不难,但是也得需要学习。
  3. SQL熟悉后,真实操作中还得把你要存储的东西,比如对象,转成SQL语句,也挺麻烦的。

indexedDB

来自MDN的解释: indexedDB 是一种低级API,用于客户端存储大量结构化数据(包括, 文件/ blobs)。该API使用索引来实现对该数据的高性能搜索。虽然 Web Storage 对于存储较少量的数据很有用,但对于存储更大量的结构化数据来说,这种方法不太有用。IndexedDB提供了一个解决方案。

所以,IndexedDB API是强大的,但对于简单的情况可能看起来太复杂了,所以要看你的业务场景来选择到底是用还是不用。

indexedDB 是一个基于JavaScript的面向对象的数据库。 IndexedDB允许你存储和检索用键索引的对象;

IndexedDB 鼓励使用的基本模式如下所示:

  • 打开数据库并且开始一个事务。
  • 创建一个 object store。
  • 构建一个请求来执行一些数据库操作,像增加或提取数据等。
  • 通过监听正确类型的 DOM 事件以等待操作完成。
  • 在操作结果上进行一些操作(可以在 request 对象中找到)

1、首先打开indexedDB数据库

语法:
window.indexedDB.open(dbName, version)

var db;
// 打开数据库,open还有第二个参数版本号
var request = window.indexedDB.open('myTestDatabase');
// 数据库打开成功后
request.onsuccess = function (event) {
    // 存储数据结果,后面所有的数据库操作都离不开它。
    db = request.result;
}
request.onerror = function (event) {
    alert("Why didn't you allow my web app to use IndexedDB?!");
}

// 数据库首次创建版本,或者window.indexedDB.open传递的新版本(版本数值要比现在的高)
request.onupgradeneeded = function (event) {

}

onupgradeneeded事件: 更新数据库的 schema,也就是创建或者删除对象存储空间,这个事件将会作为一个允许你处理对象存储空间的 versionchange 事务的一部分被调用。在数据库第一次被打开时或者当指定的版本号高于当前被持久化的数据库的版本号时,这个 versionchange 事务将被创建。onupgradeneeded 是我们唯一可以修改数据库结构的地方。在这里面,我们可以创建和删除对象存储空间以及构建和删除索引。

2、构建数据库

IndexedDB 使用对象存储空间而不是表,并且一个单独的数据库可以包含任意数量的对象存储空间。每当一个值被存储进一个对象存储空间时,它会被和一个键相关联。

  // 数据库首次创建版本,或者window.indexedDB.open传递的新版本(版本数值要比现在的高)
  request.onupgradeneeded = function (event) {

      //之前咱们不是在success中得到了db了么,为什么还要在这获取,
      //因为在当前事件函数执行后才会去执行success事件
      var db = event.target.result;

      // 创建一个对象存储空间,keyPath是id,keyGenerator是自增的
      var objectStore = db.createObjectStore('testItem',{keyPath: 'id',autoIncrement: true});
      // 创建一个索引来通过id搜索,id是自增的,不会有重复,所以可以用唯一索引
      objectStore.createIndex('id','id',{unique: true})

      objectStore.createIndex('name','name');
      objectStore.createIndex('age','age');

      //添加一条信息道数据库中
      objectStore.add({name: 'cfangxu', age: '27'});

  }

注意: 执行完后,在调试工具栏Application的indexedDB中也看不到,你得右键刷新一下。

创建索引的语法:

objectStore.createIndex(indexName, keyPath, objectParameters)

indexName:创建的索引名称,可以使用空名称作为索引。
keyPath:索引使用的关键路径,可以使用空的keyPath, 或者keyPath传为数组keyPath也是可以的。
objectParameters:可选参数。常用参数之一是unique,表示该字段值是否唯一,不能重复。例如,本demo中id是不能重复的,于是有设置:

3、添加数据

上面的代码建好了字段,并且添加了一条数据,但是我们如果想在onupgradeneeded事件外面操作,接下来的步骤了。
由于数据库的操作都是基于事务(transaction)来进行,于是,无论是添加编辑还是删除数据库,我们都要先建立一个事务(transaction),然后才能继续下面的操作。
语法: var transaction = db.transaction(dbName, "readwrite");
第一个参数是事务希望跨越的对象存储空间的列表,可以是数组或者字符串。如果你希望事务能够跨越所有的对象存储空间你可以传入一个空数组。如果你没有为第二个参数指定任何内容,你得到的是只读事务。因为这里我们是想要写入所以我们需要传入 "readwrite" 标识。

var timer = setInterval(function () {
    if(db) {
        clearInterval(timer);
        // 新建一个事务
        var transaction = db.transaction(['testItem'], 'readwrite');
        // 打开一个存储对象
        var objectStore = transaction.objectStore('testItem');
        // 添加数据到对象中
        objectStore.add({ name: 'xiaoming', age: '12' });
        objectStore.add({ name: 'xiaolong', age: '20' });
    }
},100)

为什么要用一个间隔定时器? 因为这是一个demo,正常的是要有操作才能进行数据库的写入,在我们的demo中,js执行到transaction会比indexedDB的onsuccess事件回调快,导致会拿到db为undefined,所以写了个间隔定时器等它一会。

4、获取数据

var transaction = db.transaction(['testItem'], 'readwrite');

var objectStore = transaction.objectStore('testItem');

var getRquest = objectStore.get(1);
getRquest.onsuccess = function (event) {
    console.log(getRquest.result);
}
//输出:{name: "cfangxu", age: "27", id: 1}

5、修改数据

var transaction = db.transaction(['testItem'], 'readwrite');

var objectStore = transaction.objectStore('testItem');

var getRquest = objectStore.put({ name: 'chenfangxu', age: '27', id:1 });
// 修改了id为1的那条数据

6、删除数据

var transaction = db.transaction(['testItem'], 'readwrite');

var objectStore = transaction.objectStore('testItem');

var getRquest = objectStore.delete(1);
// 删除了id为1的那条数据
上面的例子执行完后,一定一定要右键刷新indexedDB,它自己是不会变的。
查看原文

赞 135 收藏 562 评论 18

libinfs 赞了文章 · 2018-12-06

聊一聊 cookie

咱们不搞一开始就一大堆理论知识介绍,怕把人讲懵了...... 咱们换一个思维方式——"从现象看本质",先说说我们看到了什么,再从看到的现象中提出问题,最后深入寻找答案。

我们看到的 cookie

我自己创建了一个网站,网址为http://ppsc.sankuai.com。在这个网页中我设置了几个cookieJSSESSIONIDPA_VTIMEskmtutctest

在 chrome 浏览器中打开这个网站,进入开发者模式,点击Resources栏 -> 选择cookies,我们会看到如下图所示的界面:

图片描述

解释一下:左边栏Cookies下方会列举当前网页中设置过cookie的域都有哪些。上图中只有一个域,即“ppsc.sankuai.com”。而右侧区域显示的就是某个域下具体的 cookie 列表,对应上图就是“ppsc.sankuai.com”域下设置的4个cookie

在这个网页中我往http://ppsc.sankuai.com/getList接口发了一个 Ajax 请求,request header如下图所示:

图片描述

从上图中我们会看到request header中自动添加了Cookie字段(我并没有手动添加这个字段哦~),Cookie字段的值其实就是我设置的那4个 cookie。这个请求最终会发送到http://ppsc.sankuai.com这个服务器上,这个服务器就能从接收到的request header中提取那4个cookie

上面两张图展示了cookie的基本通信流程:设置cookie => cookie被自动添加到request header中 => 服务端接收到cookie。这个流程中有几个问题需要好好研究:

  1. 什么样的数据适合放在cookie中?

  2. cookie是怎么设置的?

  3. cookie为什么会自动加到request header中?

  4. cookie怎么增删查改?

我们要带着这几个问题继续往下阅读。

cookie 是怎么工作的?

首先必须明确一点,存储cookie是浏览器提供的功能。cookie 其实是存储在浏览器中的纯文本,浏览器的安装目录下会专门有一个 cookie 文件夹来存放各个域下设置的cookie

当网页要发http请求时,浏览器会先检查是否有相应的cookie,有则自动添加在request header中的cookie字段中。这些是浏览器自动帮我们做的,而且每一次http请求浏览器都会自动帮我们做。这个特点很重要,因为这关系到“什么样的数据适合存储在cookie中”。

存储在cookie中的数据,每次都会被浏览器自动放在http请求中,如果这些数据并不是每个请求都需要发给服务端的数据,浏览器这设置自动处理无疑增加了网络开销;但如果这些数据是每个请求都需要发给服务端的数据(比如身份认证信息),浏览器这设置自动处理就大大免去了重复添加操作。所以对于那设置“每次请求都要携带的信息(最典型的就是身份认证信息)”就特别适合放在cookie中,其他类型的数据就不适合了。

但在 localStorage 出现之前,cookie被滥用当做了存储工具。什么数据都放在cookie中,即使这些数据只在页面中使用而不需要随请求传送到服务端。当然cookie标准还是做了一些限制的:每个域名下的cookie 的大小最大为4KB,每个域名下的cookie数量最多为20个(但很多浏览器厂商在具体实现时支持大于20个)。

cookie 的格式

document.cookie

JS 原生的 API提供了获取cookie的方法:document.cookie(注意,这个方法只能获取非 HttpOnly 类型的cookie)。在 console 中执行这段代码可以看到结果如下图:

图片描述

打印出的结果是一个字符串类型,因为cookie本身就是存储在浏览器中的字符串。但这个字符串是有格式的,由键值对 key=value构成,键值对之间由一个分号和一个空格隔开。

cookie 的属性选项

每个cookie都有一定的属性,如什么时候失效,要发送到哪个域名,哪个路径等等。这些属性是通过cookie选项来设置的,cookie选项包括:expiresdomainpathsecureHttpOnly。在设置任一个cookie时都可以设置相关的这些属性,当然也可以不设置,这时会使用这些属性的默认值。在设置这些属性时,属性之间由一个分号和一个空格隔开。代码示例如下:

"key=name; expires=Thu, 25 Feb 2016 04:18:00 GMT; domain=ppsc.sankuai.com; path=/; secure; HttpOnly"

expires

expires选项用来设置“cookie 什么时间内有效”。expires其实是cookie失效日期,expires必须是 GMT 格式的时间(可以通过 new Date().toGMTString()或者 new Date().toUTCString() 来获得)。

expires=Thu, 25 Feb 2016 04:18:00 GMT表示cookie讲在2016年2月25日4:18分之后失效,对于失效的cookie浏览器会清空。如果没有设置该选项,则默认有效期为session,即会话cookie。这种cookie在浏览器关闭后就没有了。

expires 是 http/1.0协议中的选项,在新的http/1.1协议中expires已经由 max-age 选项代替,两者的作用都是限制cookie 的有效时间。expires的值是一个时间点(cookie失效时刻= expires),而max-age 的值是一个以为单位时间段(cookie失效时刻= 创建时刻+ max-age)。
另外,max-age 的默认值是 -1(即有效期为 session );若max-age有三种可能值:负数、0、正数。负数:有效期session0:删除cookie;正数:有效期为创建时刻+ max-age

domain 和 path

domain是域名,path是路径,两者加起来就构成了 URL,domainpath一起来限制 cookie 能被哪些 URL 访问。

一句话概括:某cookie的 domain为“baidu.com”, path为“/ ”,若请求的URL(URL 可以是js/html/img/css资源请求,但不包括 XHR 请求)的域名是“baidu.com”或其子域如“api.baidu.com”、“dev.api.baidu.com”,且 URL 的路径是“/ ”或子路径“/home”、“/home/login”,则浏览器会将此 cookie 添加到该请求的 cookie 头部中。

所以domainpath2个选项共同决定了cookie何时被浏览器自动添加到请求头部中发送出去。如果没有设置这两个选项,则会使用默认值。domain的默认值为设置该cookie的网页所在的域名,path默认值为设置该cookie的网页所在的目录。

特别说明1:
发生跨域xhr请求时,即使请求URL的域名和路径都满足 cookie 的 domain 和 path,默认情况下cookie也不会自动被添加到请求头部中。若想知道原因请阅读本文最后一节)

特别说明2:
domain是可以设置为页面本身的域名(本域),或页面本身域名的父域,但不能是公共后缀 public suffix。举例说明下:如果页面域名为 www.baidu.com, domain可以设置为“www.baidu.com”,也可以设置为“baidu.com”,但不能设置为“.com”或“com”。

secure

secure选项用来设置cookie只在确保安全的请求中才会发送。当请求是HTTPS或者其他安全协议时,包含 secure 选项的 cookie 才能被发送至服务器。

默认情况下,cookie不会带secure选项(即为空)。所以默认情况下,不管是HTTPS协议还是HTTP协议的请求,cookie 都会被发送至服务端。但要注意一点,secure选项只是限定了在安全情况下才可以传输给服务端,但并不代表你不能看到这个 cookie。

下面我们设置一个 secure类型的 cookie:

document.cookie = "name=huang; secure";

之后你就能在控制台中看到这个 cookie 了,如下图所示:

图片描述

这里有个坑需要注意下:
如果想在客户端即网页中通过 js 去设置secure类型的 cookie,必须保证网页是https协议的。在http协议的网页中是无法设置secure类型cookie的。

httpOnly

这个选项用来设置cookie是否能通过 js 去访问。默认情况下,cookie不会带httpOnly选项(即为空),所以默认情况下,客户端是可以通过js代码去访问(包括读取、修改、删除等)这个cookie的。当cookiehttpOnly选项时,客户端则无法通过js代码去访问(包括读取、修改、删除等)这个cookie

在客户端是不能通过js代码去设置一个httpOnly类型的cookie的,这种类型的cookie只能通过服务端来设置。

那我们在页面中怎么知道哪些cookiehttpOnly类型的呢?看下图:

图片描述

凡是httpOnly类型的cookie,其 HTTP 一列都会打上√,如上图中的PA_VTIME。你通过document.cookie是不能获取的,也不能修改PA_VTIME的。

——httpOnly与安全

从上面介绍中,大家是否会有这样的疑问:为什么我们要限制客户端去访问cookie?其实这样做是为了保障安全。

试想:如果任何 cookie 都能被客户端通过document.cookie获取会发生什么可怕的事情。当我们的网页遭受了 XSS 攻击,有一段恶意的script脚本插到了网页中。这段script脚本做的事情是:通过document.cookie读取了用户身份验证相关的 cookie,并将这些 cookie 发送到了攻击者的服务器。攻击者轻而易举就拿到了用户身份验证信息,于是就可以摇摇大摆地冒充此用户访问你的服务器了(因为攻击者有合法的用户身份验证信息,所以会通过你服务器的验证)。

如何设置 cookie?

知道了cookie的格式,cookie的属性选项,接下来我们就可以设置cookie了。首先得明确一点:cookie既可以由服务端来设置,也可以由客户端来设置。

服务端设置 cookie

不管你是请求一个资源文件(如 html/js/css/图片),还是发送一个ajax请求,服务端都会返回response。而response header中有一项叫set-cookie,是服务端专门用来设置cookie的。如下图所示,服务端返回的response header中有5个set-cookie字段,每个字段对应一个cookie(注意不能将多个cookie放在一个set-cookie字段中),set-cookie字段的值就是普通的字符串,每个cookie还设置了相关属性选项。

图片描述

注意:

  • 一个set-Cookie字段只能设置一个cookie,当你要想设置多个 cookie,需要添加同样多的set-Cookie字段。

  • 服务端可以设置cookie 的所有选项:expiresdomainpathsecureHttpOnly

客户端设置 cookie

在网页即客户端中我们也可以通过js代码来设置cookie。如我当前打开的网址为http://dxw.st.sankuai.com/mp/,在控制台中我们执行了下面代码:

document.cookie = "name=Jonh; ";

查看浏览器 cookie 面板如下图所示,cookie确实设置成功了,而且属性选项 domainpathexpires都用了默认值。

图片描述

再执行下面代码:

document.cookie="age=12; expires=Thu, 26 Feb 2116 11:50:25 GMT; domain=sankuai.com; path=/";

查看浏览器cookie 面板,如下图所示,新的cookie设置成功了,而且属性选项 domainpathexpires都变成了设定的值。

图片描述

注意:

  • 客户端可以设置cookie 的下列选项:expiresdomainpathsecure(有条件:只有在https协议的网页中,客户端设置secure类型的 cookie 才能成功),但无法设置HttpOnly选项。

用 js 如何设置多个 cookie

当要设置多个cookie时, js 代码很自然地我们会这么写:

document.cookie = "name=Jonh; age=12; class=111";

但你会发现这样写只是添加了第一个cookie“name=John”,后面的所有cookie都没有添加成功。所以最简单的设置多个cookie的方法就在重复执行document.cookie = "key=name",如下:

document.cookie = "name=Jonh";
document.cookie = "age=12";
document.cookie = "class=111";

如何修改、删除

修改 cookie

要想修改一个cookie,只需要重新赋值就行,旧的值会被新的值覆盖。但要注意一点,在设置新cookie时,path/domain这几个选项一定要旧cookie 保持一样。否则不会修改旧值,而是添加了一个新的 cookie。

删除 cookie

删除一个cookie 也挺简单,也是重新赋值,只要将这个新cookie的expires 选项设置为一个过去的时间点就行了。但同样要注意,path/domain/这几个选项一定要旧cookie 保持一样。

cookie 编码

cookie其实是个字符串,但这个字符串中逗号、分号、空格被当做了特殊符号。所以当cookie的 key 和 value 中含有这3个特殊字符时,需要对其进行额外编码,一般会用escape进行编码,读取时用unescape进行解码;当然也可以用encodeURIComponent/decodeURIComponent或者encodeURI/decodeURI三者的区别可以参考这篇文章)。

var key = escape("name;value");
var value = escape("this is a value contain , and ;");
document.cookie= key + "=" + value + "; expires=Thu, 26 Feb 2116 11:50:25 GMT; domain=sankuai.com; path=/";

跨域请求中 cookie

之前在介绍 XHR 的一篇文章里面提过:默认情况下,在发生跨域时,cookie 作为一种 credential 信息是不会被传送到服务端的。必须要进行额外设置才可以。具体原因和如何设置可以参考我的这篇文章:你真的会使用XMLHttpRequest吗?

另外,关于跨域资源共享 CORS极力推荐大家阅读阮一峰老师的这篇 跨域资源共享 CORS 详解

其他补充

  1. 什么时候 cookie 会被覆盖:name/domain/path 这3个字段都相同的时候;

  2. 关于domain的补充说明(参考1/参考2):

    1. 如果显式设置了 domain,则设置成什么,浏览器就存成什么;但如果没有显式设置,则浏览器会自动取 url 的 host 作为 domain 值;

    2. 新的规范中,显式设置 domain 时,如果 value 最前面带点,则浏览器处理时会将这个点去掉,所以最后浏览器存的就是没有点的(注意:但目前大多数浏览器并未全部这么实现)

    3. 前面带点‘.’和不带点‘.’有啥区别:

      • 带点:任何 subdomain 都可以访问,包括父 domain

      • 不带点:只有完全一样的域名才能访问,subdomain 不能(但在 IE 下比较特殊,它支持 subdomain 访问)

总结

咱们今天就聊到这里,若有不对之处欢迎各位指正~~
最后附上一些参考资料:

  1. http://www.quirksmode.org/js/...

  2. http://www.tutorialspoint.com...

  3. http://www.allaboutcookies.or...

  4. http://bubkoo.com/2014/04/21/...

查看原文

赞 325 收藏 530 评论 50

libinfs 赞了回答 · 2018-12-06

解决一个注册域名能分配多少子域名?

我其实是顺着题主的另外一个问题爬过来的,一开始没打算写一个新答案,只是在其中一个答案里评论了一下。之后忽然觉得我所知道的貌似出处皆不可考,于是担心自己也错了,遂查阅权威资料寻找答案,最后的结果是:

以上答案都错了(都有错误的部分)——包括我自己之前的评论(捂脸……)

这里就不长篇大论了,细节在 wikiphdia 上都有(别说维基谁都可以改,我也核对了在 ICANN,CNNIC 等权威机构的相关解释,而且 IETF 发表的几个规范也解释得很清楚——这些资源维基都有提供)。在此我只简述我们普遍理解错误的几个概念:

a.net 为例:

  1. 何为一级域名net 是一级域名
  2. 何为二级域名a.net 是二级域名
  3. xxx.a.net为何物:它其实是三级域名

也就是说,我们惯常所说的二级域名实际上应该是三级域名。当然,这些概念和题主的问题并无直接关系,但随后的答案在这点上竟然都是错的,而且还因此和题主产生了很多分歧与矛盾,渐渐地反而离主题越来越远。

这是一个令人惊讶的结果,原本多数人以为题主没有理解的概念在事实上我们都理解错了,当然我也不确定题主理解的是否正确,因为如果假设题主理解的正确,原本的问题应该也很容易得到答案。上面提供的维基地址里就有。

简单回答一下:

  1. 通常我们花钱去注册的是二级域名,因此除非再额外花钱,否则我们暂定只有一个二级域名可用。
  2. 那么这个二级域名能用来解析几个网站?一个。

    1. 通俗地说,域名是用来“解释”物理地址——IP 的,因而一个二级域名当然只能对应一个网站,因为每一个独立的网站都需要至少一个物理地址才能存在于互联网上供人访问。
  3. 当你拥有一个能完全掌控的二级域名时,你可以建立不限数目的三级域名,当然它们的二级域名部分是一样的。

    1. 三级域名也可以用来解析物理 IP 地址,所以你可以将它使用在和二级域名完全不同的网站上。
    2. 因此,你的确可以只购买一个二级域名,但是却可以建立无数个网站,它们分别使用不同的三级域名
  4. 三级域名不是终点,还有四级五级,……更多级可用。

    1. 然而每一级域名到底能设置多少个,理论上没有限制,但实际上域名的提供商是可以做限制的,这一点在购买域名的时候要留心。

说到这里,题主的问题应该已经得以解决了吧?简而言之,你问题里提出的假设是对的。即:“只购买一个二级域名,想做多少网站就做多少网站”,不过域名只是网站的名片而已,具体做网站还要空间,要流量,要 IP 等东东,额外花钱还是少不了的。这些事情我想题主已经清楚了吧?


补充一点,回头看了一下原题的标题……不得不说题主你这是自相矛盾啊!标题本身和问题描述的设想相互打架了你没发现吗?

对于问题,以上回答已经解释了:你购买一个(二级)域名,可以建立无限个网站,因为你可以在此基础上创建更多低级的子域名。

但是你的标题:

一个域名能分配多少主机名/网站名?

这和问题里的描述完全是两回事啊。

域名,是一个通用概念,一个二级域名是一个域名,一个三级域名也是一个域名。具体到每一个特指的域名,它的确只能“分配”(说对应更合适)一个主机名。

怎么说?

a.net,这是一个域名;xxx.a.net,这也是一个域名(虽然它们层级不同)。
a.net,这是一个主机名;xxx.a.net,这也是一个主机名(它们可以是解析同一个主机,也可以是不同的,这取决于你如何设置解析它们的规则)。

不过我可以理解,在你问问题的时候,你心里想的域名是 a.net,也就是要花钱的那个;其他的,比如 xxx.a.netyyy.a.net 等等才是主机名。所以你才会有这样的问题标题和这样的问题内容。

怎样?现在还觉得提问或回答是应该“能多简单就多简单”吗?

关注 19 回答 7

libinfs 收藏了文章 · 2018-11-30

【译】只用 CSS 就能做到的像素画/像素动画

只用 CSS 就能做到的像素画/像素动画

clipboard.png

原文链接:box-shadowを使ってCSSだけでドット絵を描き、アニメーションさせる
作者推特:bc_rikko
作者的推特里面有不少例子,有能力的同学可以看一下
翻译博客地址:https://ssshooter.com/css-pixel/

这篇文章将会介绍只用 CSS 就能制作像素画·像素动画的方法。虽说纯 CSS 就能做到,但是为了更高的可维护性,也会顺便介绍使用 Sass 的制作方法。

clipboard.png

clipboard.png

上面的马里奥和 Minecraft 方块都没有使用 JavaScript,单纯使用 CSS 动画制作。

关于 box-shadow 属性

绘制像素点可以借助 box-shadow 属性。
原本 box-shadow 属性用于制作阴影效果,先介绍一下基本用法。

该属性的写法有几种:

  • box-shadow: offset-x offset-y color
  • box-shadow: offset-x offset-y blur-radius color
  • box-shadow: offset-x offset-y blur-radius spread-radius color
  • box-shadow: inset offset-x offset-y color

offset-xoffset-y 用于指定阴影偏移位置。以元素的左上角为原点,指定 XY 轴移动的位置。
color 字面意思,指定阴影颜色。
blur-radius 指定模糊效果的半径。跟 border-radius 差不多。
spread-raduis 模糊范围的扩大与缩小。
inset 关键字可以使阴影效果显示在元素内则。

文字说明或许不够形象,我们可以直接看效果:

https://jsfiddle.net/bc_rikko...

实际效果如下,每个值会造成什么影响应该能很直观地看懂。

基础:描绘一个像素点

box-shadow 基础都明白了,就可以进入下一步:描绘一个像素点。
对一个边长 100px 的正方形使用 box-shadow

<div class="container">
    <div class="box"></div>
</div>

<style>
* {
  /* 为了方便看到元素而添加的边框(不加也行) */
  box-sizing: border-box;
}
.container {
  /* 长和宽包括 box-shadow */
  width: 200px;
  height: 200px;
}

.box {
  /* 元素属性 */
  width: 100px;
  height: 100px;
  border: 2px solid #777;

  /* 在元素右下角相同大小的方块 */
  box-shadow: 100px 100px rgba(7,7,7,.3);
}
</style>

clipboard.png

如图所示,使用 box-shadow 描绘了一个与元素相同大小的阴影。代码的意思是把一个 100px 的方形的影子放到 (100px, 100px) 的位置。

进阶:用 box-shadow 属性绘制像素画

完成预想图

完成预想图
这两个都是 5✖️5 的像素画,我们先从左边开始:

<div class="container">
  <div class="pixel one"></div>
</div>

<style>
.container {
  /* 像素画的大小 */
  width: 100px;
  height: 100px;
}

.pixel {
  /* 使伪元素的位置可调整 */
  position: relative;
}
.pixel::before {
  content: "";

  /* 一个点的大小(例:20px x 20px) */
  width: 20px;
  height: 20px;
  /* box-shadow 着色,伪元素设为透明 */
  background-color: transparent;

  /* 调整伪元素位置,让左上角成为(0,0) */
  position: absolute;
  top: -20px;
  left: -20px;
}

.pixel.one::before {
  box-shadow:
     /* 列 行 色 */
     /* 第1列 */
     20px   20px #FB0600,
     20px   40px #FC322F,
     20px   60px #FC6663,
     20px   80px #FD9999,
     20px  100px #FECCCB, 
     /* 第2列 */
     40px   20px #60169F,
     40px   40px #7A23B0,
     40px   60px #964DC2,
     40px   80px #B681D9,
     40px  100px #D8BEED, 
     /* 第3列 */
     60px   20px #1388BC,
     60px   40px #269DC9,
     60px   60px #55B3D7,
     60px   80px #88CAE2,
     60px  100px #BFE3EF, 
     /* 第4列 */
     80px   20px #ACD902,
     80px   40px #BDE02D,
     80px   60px #CDEA5E,
     80px   80px #DBEF8E,
     80px  100px #F4FBC8, 
     /* 第5列 */
    100px  20px #FB8F02,
    100px  40px #FDA533,
    100px  60px #FDBB64,
    100px  80px #FED39A,
    100px 100px #FDE8C9;
}
</style>

首先,box-shadow 生产的影子大小不包括本体元素的大小,container 类的大小设为像素画完成后的大小就行。
接着,box-shadow 的影子大小由,pixel 类的大小决定,所以把 widthheight设定为 20px。
实际的点是 before 伪元素绘制的,pixel 的 20px 正方形会在左上角留下空位,为此可以使用 position: absolute 调整。
最后使用 box-shadow 逐格绘制像素画。

接着实现右边的像素画。

.pixel.two::before {
  box-shadow:
    20px   20px #704b16,
    40px   20px #704b16,
    60px   20px #704b16,
    80px   20px #704b16,
    100px  20px #704b16,
    20px   40px #704b16,
    40px   40px #fdb778,
    60px   40px #fdb778,
    80px   40px #fdb778,
    100px  40px #704b16,
    20px   60px #fdb778,
    40px   60px #333333,
    60px   60px #fdb778,
    80px   60px #333333,
    100px  60px #fdb778,
    20px   80px #fdb778,
    40px   80px #fdb778,
    60px   80px #fdb778,
    80px   80px #fdb778,
    100px  80px #fdb778,
    20px  100px #fdb778,
    40px  100px #c70300,
    60px  100px #c70300,
    80px  100px #c70300,
    100px 100px #fdb778;
}

应用:使用 Sass 编写可维护像素画

上面写的几个例子,至少我是没什么信心去维护好他们。5x5 的像素画要写 25 次属性值,一般的 16x16 则是多达 256 个值。
所以,我们可以使用 Sass 编写可维护像素画。
Sass 环境搭建可以参考以下文章(日语)
https://kuroeveryday.blogspot...

Sass 使用 mixin(function 亦可)生成样式的方法:

@mixin pixelize($matrix, $size, $colors) {
  $ret: "";

  @for $i from 1 through length($matrix) {
    $row: nth($matrix, $i);

    @for $j from 1 through length($row) {
      $dot: nth($row, $j);

      @if $dot != 0 {
        @if $ret != "" {
          $ret: $ret + ",";
        }

        $color: nth($colors, $dot);
        $ret: $ret + ($j * $size) + " " + ($i * $size) + " " + $color;
      }
    }
  }

  box-shadow: unquote($ret + ";");
}

$heart-colors: (#333, #f11416, #831200);
$heart: (
  (0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0),
  (0,0,1,1,1,0,0,0,0,0,1,1,1,0,0,0),
  (0,1,2,2,2,1,0,0,0,1,2,2,3,1,0,0),
  (1,2,0,0,2,2,1,0,1,2,2,2,2,3,1,0),
  (1,2,0,2,2,2,2,1,2,2,2,2,2,3,1,0),
  (1,2,2,2,2,2,2,2,2,2,2,2,2,3,1,0),
  (1,2,2,2,2,2,2,2,2,2,2,2,2,3,1,0),
  (1,2,2,2,2,2,2,2,2,2,2,2,2,3,1,0),
  (0,1,2,2,2,2,2,2,2,2,2,2,3,1,0,0),
  (0,0,1,2,2,2,2,2,2,2,2,3,1,0,0,0),
  (0,0,0,1,2,2,2,2,2,2,3,1,0,0,0,0),
  (0,0,0,0,1,2,2,2,2,3,1,0,0,0,0,0),
  (0,0,0,0,0,1,2,2,3,1,0,0,0,0,0,0),
  (0,0,0,0,0,0,1,3,1,0,0,0,0,0,0,0),
  (0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0),
  (0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)
);

.icon {
  width: 20px;
  height: 20px;
  @include pixelize($heart, 20px, $heart-colors);
}

定义名为 pixelize 的 mixin,把像素画的矩阵($heart)像素点的大小(20px)颜色列表($hearts-colors)传入其中,即可生成 box-shadow 属性。
像素画的矩阵用数字 0~N 表示,0 为透明,1~n 为颜色列表对应颜色。

如果有代码高亮的话,像素画的图案就一目了然啦。

与原生 CSS 相比,这样简单多了吧?

如果这样都觉得麻烦,可以使用 CSS 像素画生成器~

CSSドット絵ジェネレータ

番外篇:制作像素动画

之前 icon 类直接使用 box-shadow 属性绘制像素画,在制作像素动画时,需要使用 CSS animation。

.mario {
  width: 8px;
  height: 8px;

  animation:
    jump 1s infinite,
    sprite 1s infinite;
}

/* 跳跃动作(上下移動) */
@keyframes jump {
  from, 25%, 75%, to {
    transform: translateY(0);
  }
  50% {
    transform: translateY(calc(8px * -8));
  }
}

/* 普通状态和跳跃状态的像素画 */
@keyframes sprite {
  /* 对比 animation-timing-function: steps(n)
   * 使用百分比可以更细致的调整动画时间 
   */
  from, 24%, 76%, to {
    box-shadow: /* 普通状态的像素画 */
  }
  25%, 75% {
    box-shadow: /* 跳跃状态的像素画 */
  }
}

使用 CSS 动画修改 box-shadow 和元素的位置,看起来就像是跳起来一样。
详细代码可以在 github 仓库中了解
https://github.com/BcRikko/cs...

查看原文

libinfs 赞了文章 · 2018-11-30

【译】只用 CSS 就能做到的像素画/像素动画

只用 CSS 就能做到的像素画/像素动画

clipboard.png

原文链接:box-shadowを使ってCSSだけでドット絵を描き、アニメーションさせる
作者推特:bc_rikko
作者的推特里面有不少例子,有能力的同学可以看一下
翻译博客地址:https://ssshooter.com/css-pixel/

这篇文章将会介绍只用 CSS 就能制作像素画·像素动画的方法。虽说纯 CSS 就能做到,但是为了更高的可维护性,也会顺便介绍使用 Sass 的制作方法。

clipboard.png

clipboard.png

上面的马里奥和 Minecraft 方块都没有使用 JavaScript,单纯使用 CSS 动画制作。

关于 box-shadow 属性

绘制像素点可以借助 box-shadow 属性。
原本 box-shadow 属性用于制作阴影效果,先介绍一下基本用法。

该属性的写法有几种:

  • box-shadow: offset-x offset-y color
  • box-shadow: offset-x offset-y blur-radius color
  • box-shadow: offset-x offset-y blur-radius spread-radius color
  • box-shadow: inset offset-x offset-y color

offset-xoffset-y 用于指定阴影偏移位置。以元素的左上角为原点,指定 XY 轴移动的位置。
color 字面意思,指定阴影颜色。
blur-radius 指定模糊效果的半径。跟 border-radius 差不多。
spread-raduis 模糊范围的扩大与缩小。
inset 关键字可以使阴影效果显示在元素内则。

文字说明或许不够形象,我们可以直接看效果:

https://jsfiddle.net/bc_rikko...

实际效果如下,每个值会造成什么影响应该能很直观地看懂。

基础:描绘一个像素点

box-shadow 基础都明白了,就可以进入下一步:描绘一个像素点。
对一个边长 100px 的正方形使用 box-shadow

<div class="container">
    <div class="box"></div>
</div>

<style>
* {
  /* 为了方便看到元素而添加的边框(不加也行) */
  box-sizing: border-box;
}
.container {
  /* 长和宽包括 box-shadow */
  width: 200px;
  height: 200px;
}

.box {
  /* 元素属性 */
  width: 100px;
  height: 100px;
  border: 2px solid #777;

  /* 在元素右下角相同大小的方块 */
  box-shadow: 100px 100px rgba(7,7,7,.3);
}
</style>

clipboard.png

如图所示,使用 box-shadow 描绘了一个与元素相同大小的阴影。代码的意思是把一个 100px 的方形的影子放到 (100px, 100px) 的位置。

进阶:用 box-shadow 属性绘制像素画

完成预想图

完成预想图
这两个都是 5✖️5 的像素画,我们先从左边开始:

<div class="container">
  <div class="pixel one"></div>
</div>

<style>
.container {
  /* 像素画的大小 */
  width: 100px;
  height: 100px;
}

.pixel {
  /* 使伪元素的位置可调整 */
  position: relative;
}
.pixel::before {
  content: "";

  /* 一个点的大小(例:20px x 20px) */
  width: 20px;
  height: 20px;
  /* box-shadow 着色,伪元素设为透明 */
  background-color: transparent;

  /* 调整伪元素位置,让左上角成为(0,0) */
  position: absolute;
  top: -20px;
  left: -20px;
}

.pixel.one::before {
  box-shadow:
     /* 列 行 色 */
     /* 第1列 */
     20px   20px #FB0600,
     20px   40px #FC322F,
     20px   60px #FC6663,
     20px   80px #FD9999,
     20px  100px #FECCCB, 
     /* 第2列 */
     40px   20px #60169F,
     40px   40px #7A23B0,
     40px   60px #964DC2,
     40px   80px #B681D9,
     40px  100px #D8BEED, 
     /* 第3列 */
     60px   20px #1388BC,
     60px   40px #269DC9,
     60px   60px #55B3D7,
     60px   80px #88CAE2,
     60px  100px #BFE3EF, 
     /* 第4列 */
     80px   20px #ACD902,
     80px   40px #BDE02D,
     80px   60px #CDEA5E,
     80px   80px #DBEF8E,
     80px  100px #F4FBC8, 
     /* 第5列 */
    100px  20px #FB8F02,
    100px  40px #FDA533,
    100px  60px #FDBB64,
    100px  80px #FED39A,
    100px 100px #FDE8C9;
}
</style>

首先,box-shadow 生产的影子大小不包括本体元素的大小,container 类的大小设为像素画完成后的大小就行。
接着,box-shadow 的影子大小由,pixel 类的大小决定,所以把 widthheight设定为 20px。
实际的点是 before 伪元素绘制的,pixel 的 20px 正方形会在左上角留下空位,为此可以使用 position: absolute 调整。
最后使用 box-shadow 逐格绘制像素画。

接着实现右边的像素画。

.pixel.two::before {
  box-shadow:
    20px   20px #704b16,
    40px   20px #704b16,
    60px   20px #704b16,
    80px   20px #704b16,
    100px  20px #704b16,
    20px   40px #704b16,
    40px   40px #fdb778,
    60px   40px #fdb778,
    80px   40px #fdb778,
    100px  40px #704b16,
    20px   60px #fdb778,
    40px   60px #333333,
    60px   60px #fdb778,
    80px   60px #333333,
    100px  60px #fdb778,
    20px   80px #fdb778,
    40px   80px #fdb778,
    60px   80px #fdb778,
    80px   80px #fdb778,
    100px  80px #fdb778,
    20px  100px #fdb778,
    40px  100px #c70300,
    60px  100px #c70300,
    80px  100px #c70300,
    100px 100px #fdb778;
}

应用:使用 Sass 编写可维护像素画

上面写的几个例子,至少我是没什么信心去维护好他们。5x5 的像素画要写 25 次属性值,一般的 16x16 则是多达 256 个值。
所以,我们可以使用 Sass 编写可维护像素画。
Sass 环境搭建可以参考以下文章(日语)
https://kuroeveryday.blogspot...

Sass 使用 mixin(function 亦可)生成样式的方法:

@mixin pixelize($matrix, $size, $colors) {
  $ret: "";

  @for $i from 1 through length($matrix) {
    $row: nth($matrix, $i);

    @for $j from 1 through length($row) {
      $dot: nth($row, $j);

      @if $dot != 0 {
        @if $ret != "" {
          $ret: $ret + ",";
        }

        $color: nth($colors, $dot);
        $ret: $ret + ($j * $size) + " " + ($i * $size) + " " + $color;
      }
    }
  }

  box-shadow: unquote($ret + ";");
}

$heart-colors: (#333, #f11416, #831200);
$heart: (
  (0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0),
  (0,0,1,1,1,0,0,0,0,0,1,1,1,0,0,0),
  (0,1,2,2,2,1,0,0,0,1,2,2,3,1,0,0),
  (1,2,0,0,2,2,1,0,1,2,2,2,2,3,1,0),
  (1,2,0,2,2,2,2,1,2,2,2,2,2,3,1,0),
  (1,2,2,2,2,2,2,2,2,2,2,2,2,3,1,0),
  (1,2,2,2,2,2,2,2,2,2,2,2,2,3,1,0),
  (1,2,2,2,2,2,2,2,2,2,2,2,2,3,1,0),
  (0,1,2,2,2,2,2,2,2,2,2,2,3,1,0,0),
  (0,0,1,2,2,2,2,2,2,2,2,3,1,0,0,0),
  (0,0,0,1,2,2,2,2,2,2,3,1,0,0,0,0),
  (0,0,0,0,1,2,2,2,2,3,1,0,0,0,0,0),
  (0,0,0,0,0,1,2,2,3,1,0,0,0,0,0,0),
  (0,0,0,0,0,0,1,3,1,0,0,0,0,0,0,0),
  (0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0),
  (0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)
);

.icon {
  width: 20px;
  height: 20px;
  @include pixelize($heart, 20px, $heart-colors);
}

定义名为 pixelize 的 mixin,把像素画的矩阵($heart)像素点的大小(20px)颜色列表($hearts-colors)传入其中,即可生成 box-shadow 属性。
像素画的矩阵用数字 0~N 表示,0 为透明,1~n 为颜色列表对应颜色。

如果有代码高亮的话,像素画的图案就一目了然啦。

与原生 CSS 相比,这样简单多了吧?

如果这样都觉得麻烦,可以使用 CSS 像素画生成器~

CSSドット絵ジェネレータ

番外篇:制作像素动画

之前 icon 类直接使用 box-shadow 属性绘制像素画,在制作像素动画时,需要使用 CSS animation。

.mario {
  width: 8px;
  height: 8px;

  animation:
    jump 1s infinite,
    sprite 1s infinite;
}

/* 跳跃动作(上下移動) */
@keyframes jump {
  from, 25%, 75%, to {
    transform: translateY(0);
  }
  50% {
    transform: translateY(calc(8px * -8));
  }
}

/* 普通状态和跳跃状态的像素画 */
@keyframes sprite {
  /* 对比 animation-timing-function: steps(n)
   * 使用百分比可以更细致的调整动画时间 
   */
  from, 24%, 76%, to {
    box-shadow: /* 普通状态的像素画 */
  }
  25%, 75% {
    box-shadow: /* 跳跃状态的像素画 */
  }
}

使用 CSS 动画修改 box-shadow 和元素的位置,看起来就像是跳起来一样。
详细代码可以在 github 仓库中了解
https://github.com/BcRikko/cs...

查看原文

赞 38 收藏 24 评论 0

libinfs 赞了文章 · 2018-11-29

浏览器缓存原理以及本地存储

作为一名前端工作人员,前端的缓存知识是必须掌握的,因为一个网站打开网页的速度直接关系到用户体验,用户粘度,而提高网页的打开速度有很多方面需要优化,其中比较重要的一点就是利用好缓存,缓存文件可以重复利用,还可以减少带宽,降低网络负荷。

1 缓存

缓存从宏观上分为私有缓存和共享缓存,共享缓存就是那些能被各级代理缓存的缓存。私有缓存就是用户专享的,各级代理不能缓存的缓存。

缓存从微观上可以分为以下几类:

  • 浏览器缓存
  • 代理服务器缓存
  • CDN缓存
  • 数据库缓存
  • 应用层缓存

这里主要对浏览器的缓存进行说明:

clipboard.png

2 http缓存

2.1 强缓存

  • 不会向服务器发送请求,直接从缓存中读取资源
  • 请求返回200的状态码
  • 在chrome控制台的network选项中可以看到size显示from disk cache或from memory cache。
from memory cache代表使用内存中的缓存,from disk cache则代表使用的是硬盘中的缓存,浏览器读取缓存的顺序为memory –> disk。在浏览器中,浏览器会在js和图片等文件解析执行后直接存入内存缓存中,那么当刷新页面时只需直接从内存缓存中读取(from memory cache);而css文件则会存入硬盘文件中,所以每次渲染页面都需要从硬盘读取缓存(from disk cache)。

clipboard.png

Expires和Cache-Control两者对比:其实这两者差别不大,区别就在于 Expires 是http1.0的产物,Cache-Control是http1.1的产物,两者同时存在的话,Cache-Control优先级高于Expires

2.2 协商缓存

协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程
  • 协商缓存生效,返回304和Not Modified

clipboard.png

2.2.1 Last-Modified和If-Modified-Since

浏览器在第一次访问资源时,服务器返回资源的同时,在response header中添加 Last-Modified的header,值是这个资源在服务器上的最后修改时间,浏览器接收后缓存文件和header;

浏览器下一次请求这个资源,浏览器检测到有 Last-Modified这个header,于是添加If-Modified-Since这个header,值就是Last-Modified中的值;服务器再次收到这个资源请求,会根据 If-Modified-Since 中的值与服务器中这个资源的最后修改时间对比,如果没有变化,返回304和空的响应体,直接从缓存读取,如果If-Modified-Since的时间小于服务器中这个资源的最后修改时间,说明文件有更新,于是返回新的资源文件和200

缺点:1、某些服务端不能获取精确的修改时间 2、文件修改时间改了,但文件内容却没有变

2.2.2 ETag和If-None-Match

Etag是上一次加载资源时,服务器返回的response header,是对该资源的一种唯一标识,只要资源有变化,Etag就会重新生成。浏览器在下一次加载资源向服务器发送请求时,会将上一次返回的Etag值放到request header里的If-None-Match里,服务器只需要比较客户端传来的If-None-Match跟自己服务器上该资源的ETag是否一致,就能很好地判断资源相对客户端而言是否被修改过了。如果服务器发现ETag匹配不上,那么直接以常规GET 200回包形式将新的资源(当然也包括了新的ETag)发给客户端;如果ETag是一致的,则直接返回304知会客户端直接使用本地缓存即可。

2.2.3 协商缓存两种方式的对比

  1. 首先在精确度上,Etag要优于Last-Modified,Last-Modified的时间单位是秒,如果某个文件在1秒内改变了多次,那么他们的Last-Modified其实并没有体现出来修改,但是Etag每次都会改变确保了精度;如果是负载均衡的服务器,各个服务器生成的Last-Modified也有可能不一致。
  2. 性能上,Etag要逊于Last-Modified,毕竟Last-Modified只需要记录时间,而Etag需要服务器通过算法来计算出一个hash值。
  3. 优先级上,服务器校验优先考虑Etag

3 缓存机制

appcache优先于强缓存,强制缓存优先于协商缓存进行,若强制缓存(Expires和Cache-Control)生效则直接使用缓存,若不生效则进行协商缓存(Last-Modified / If-Modified-Since和Etag / If-None-Match),协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,返回200,重新返回资源和缓存标识,再存入浏览器缓存中;生效则返回304,继续使用缓存。具体流程看下图:

clipboard.png

不管是浏览器缓存,还是代理服务器缓存,CDN缓存都遵循客户端与服务端之间的缓存机制

4、本地存储

本地存储主要有以下几种,localStorage,sessionStorage和cookie,WebSql和IndexDB主要用在前端有大容量存储需求的页面上,例如,在线编辑浏览器或者网页邮箱。他们都可以将数据存储在浏览器,应该根据不同的场景进行使用。

4.1 Cookie

Cookie主要是由服务器生成,且前端也可以设置,保存在客户端本地的一个文件,通过response响应头的set-Cookie字段进行设置,且Cookie的内容自动在请求的时候被传递给服务器。如下:

[HTTP/1.1 200 OK]
Server:[bfe/1.0.8.18]
Etag:["58860415-98b"]
Cache-Control:[private, no-cache, no-store, proxy-revalidate, no-transform]
Connection:[Keep-Alive]
Set-Cookie:[BDORZ=27315; max-age=86400; domain=.baidu.com; path=/]
Pragma:[no-cache]
Last-Modified:[Mon, 23 Jan 2017 13:24:37 GMT]
Content-Length:[2443]
Date:[Mon, 09 Apr 2018 09:59:06 GMT]
Content-Type:[text/html]

Cookie包含的信息:
它可以记录你的用户ID、密码、浏览过的网页、停留的时间等信息。当你再次来到该网站时,网站通过读取Cookies,得知你的相关信息,就可以做出相应的动作,如在页面显示欢迎你的标语,或者让你不用输入ID、密码就直接登录等等。一个网站只能读取它自己放置的信息,不能读取其他网站的Cookie文件。因此,Cookie文件还保存了host属性,即网站的域名或ip。
这些属性以名值对的方式进行保存,为了安全,它的内容大多进行了加密处理。Cookie文件的命名格式是:用户名@网站地址[数字].txt

Cookie的优点:

  • 给用户更人性化的使用体验,如记住“密码功能”、老用户登录欢迎语
  • 弥补了HTTP无连接特性
  • 站点统计访问人数的一个依据

Cookie的缺点:

  • 它无法解决多人共用一台电脑的问题,带来了不安全因素
  • Cookie文件容易被误删除
  • 一人使用多台电脑
  • Cookies欺骗。修改host文件,可以非法访问目标站点的Cookie
  • 容量有限制,不能超过4kb
  • 在请求头上带着数据安全性差

4.2 localStorage

localStorage主要是前端开发人员,在前端设置,一旦数据保存在本地后,就可以避免再向服务器请求数据,因此减少不必要的数据请求,减少数据在浏览器和服务器间不必要地来回传递。

可以长期存储数据,没有时间限制,一天,一年,两年甚至更长,数据都可以使用。
localStorage中一般浏览器支持的是5M大小,这个在不同的浏览器中localStorage会有所不同

优点:

  • localStorage拓展了cookie的4k限制
  • localStorage可以将第一次请求的5M大小数据直接存储到本地,相比于cookie可以节约带宽
  • localStorage的使用也是遵循同源策略的,所以不同的网站直接是不能共用相同的localStorage

缺点:

  • 需要手动删除,否则长期存在
  • 浏览器大小不一,版本的支持也不一样
  • localStorage只支持string类型的存储,JSON对象需要转换
  • localStorage本质上是对字符串的读取,如果存储内容多的话会消耗内存空间,会导致页面变卡

4.3 sessionStorage

sessionStorage主要是前端开发人员,在前端设置,sessionStorage(会话存储),只有在浏览器被关闭之前使用,创建另一个页面时同意可以使用,关闭浏览器之后数据就会消失

存储上限限制:不同的浏览器存储的上限也不一样,但大多数浏览器把上限限制在5MB以下

4.4 websql

Web SQL 是在浏览器上模拟数据库,可以使用JS来操作SQL完成对数据的读写。它使用 SQL 来操纵客户端数据库的 API,这些 API 是异步的,规范中使用的方言是SQLlite。数据库还是在服务端,不建议使用,已废弃

4.5 indexDB

随着浏览器的功能不断增强,越来越多的网站开始考虑,将大量数据储存在客户端,这样可以减少从服务器获取数据,直接从本地获取数据。

现有的浏览器数据储存方案,都不适合储存大量数据:Cookie 的大小不超过4KB,且每次请求都会发送回服务器;LocalStorage 在 2.5MB 到 10MB 之间(各家浏览器不同),而且不提供搜索功能,不能建立自定义的索引。所以,需要一种新的解决方案,这就是 IndexedDB 诞生的背景。

通俗地说,IndexedDB 就是浏览器提供的本地数据库,它可以被网页脚本创建和操作。IndexedDB 允许储存大量数据,提供查找接口,还能建立索引。这些都是 LocalStorage 所不具备的。就数据库类型而言,IndexedDB 不属于关系型数据库(不支持 SQL 查询语句),更接近 NoSQL 数据库。

关于indexDB的知识可以查看这篇文章http://www.ruanyifeng.com/blo...

这里,我只是根据自己的理解整理了一下关于缓存,存储方面的知识,还有很多不足的地方,更多实践的知识,还请查看其他文章,如有错误,请指出

参考文章:
https://www.jianshu.com/p/54c...
https://segmentfault.com/a/11...
http://www.cnblogs.com/etoah/...
https://blog.csdn.net/zhouche...

查看原文

赞 121 收藏 96 评论 6

libinfs 收藏了文章 · 2018-09-24

Hybrid App技术解析 -- 原理篇

引言

随着 Web 技术和移动设备的快速发展,Hybrid 技术已经成为一种最主流最常见的方案。一套好的 Hybrid架构方案 能让 App 既能拥有极致的体验和性能,同时也能拥有 Web技术 灵活的开发模式、跨平台能力以及热更新机制,想想是不是都鸡冻不已。。😄。本系列文章是公司在这方面实践的一个总结,包含了原理解析、方案选型与实现、实践优化等方面。

大家可以到github上和我进行讨论哈!

第二篇实战篇 也已经完成了哈~~

现有混合方案

Hybrid App,俗称混合应用,即混合了 Native技术 与 Web技术 进行开发的移动应用。现在比较流行的混合方案主要有三种,主要是在UI渲染机制上的不同:

  1. 基于 WebView UI 的基础方案,市面上大部分主流 App 都有采用,例如微信JS-SDK,通过 JSBridge 完成 H5 与 Native 的双向通讯,从而赋予H5一定程度的原生能力。
  2. 基于 Native UI 的方案,例如 React-Native、Weex。在赋予 H5 原生API能力的基础上,进一步通过 JSBridge 将js解析成的虚拟节点树(Virtual DOM)传递到 Native 并使用原生渲染。
  3. 另外还有近期比较流行的小程序方案,也是通过更加定制化的 JSBridge,并使用双 WebView 双线程的模式隔离了JS逻辑与UI渲染,形成了特殊的开发模式,加强了 H5 与 Native 混合程度,提高了页面性能及开发体验。

以上的三种方案,其实同样都是基于 JSBridge 完成的通讯层,第二三种方案,其实可以看做是在方案一的基础上,继续通过不同的新技术进一步提高了应用的混合程度。因此,JSBridge 也是整个混合应用最关键的部分,例如我们在设置微信分享时用到的 JS-SDK,wx对象 便是我们最常见的 JSBridge:

图片描述

方案选型

任何技术方案的选型,其实都应该基于使用场景和现有条件。基于公司现有情况的几点考虑,在方案一上进一步优化,更加适合我们的需求。

  • 需求 Web技术 快速迭代、灵活开发的特点和线上热更新的机制。
  • 产品的核心能力是强大的拍照与底层图片处理能力,因此单纯的 H5技术能做的事非常有限,不能满足需求,通过 Hybrid 技术来强化H5,便是一种必需。
  • 公司业务上,并没有非常复杂的UI渲染需求,而且 App 中的一系列原生 UI组件 已经非常成熟,因此我们并不强需类似 RN 这样的方案。

因此,如何既能利用 H5 强大的开发和迭代能力,又能赋予 H5 强大的底层能力和用户体验,同时能复用现有的成熟 Native组件,便成为了我们最大的需求点 -- 一套完整又强大的 Hybrid技术架构方案。😠

Hybrid技术原理

Hybrid App的本质,其实是在原生的 App 中,使用 WebView 作为容器直接承载 Web页面。因此,最核心的点就是 Native端 与 H5端 之间的双向通讯层,其实这里也可以理解为我们需要一套跨语言通讯方案,来完成 Native(Java/Objective-c/...) 与 JavaScript 的通讯。这个方案就是我们所说的 JSBridge,而实现的关键,便是作为容器的 WebView,一切的原理都是基于 WebView 的机制。

图片描述

(一) JavaScript 通知 Native

基于 WebView 的机制和开放的 API, 实现这个功能有三种常见的方案:

  • API注入,原理其实就是 Native 获取 JavaScript环境上下文,并直接在上面挂载对象或者方法,使 js 可以直接调用,Android 与 IOS 分别拥有对应的挂载方式。
  • WebView 中的 prompt/console/alert 拦截,通常使用 prompt,因为这个方法在前端中使用频率低,比较不会出现冲突;
  • WebView URL Scheme 跳转拦截

第二三种机制的原理是类似的,都是通过对 WebView 信息冒泡传递的拦截,从而达到通讯的,接下来我们主要从 原理-定制协议-拦截协议-参数传递-回调机制 5个方面详细阐述下第三种方案 -- URL拦截方案。

1. 实现原理

在 WebView 中发出的网络请求,客户端都能进行监听和捕获

2. 协议的定制

我们需要制定一套URL Scheme规则,通常我们的请求会带有对应的协议开头,例如常见的 https://xxx.com 或者 file://1.jpg,代表着不同的含义。我们这里可以将协议类型的请求定制为:

xxcommand://xxxx?param1=1&param2=2

这里有几个需要注意点的是:

(1) xxcommand:// 只是一种规则,可以根据业务进行制定,使其具有含义,例如我们定义 xxcommand:// 为公司所有App系通用,为通用工具协议:

xxcommand://getProxy?h=1

而定义 xxapp:// 为每个App单独的业务协议。

xxapp://openCamera?h=2

不同的协议头代表着不同的含义,这样便能清楚知道每个协议的适用范围。

(2) 这里不要使用 location.href 发送,因为其自身机制有个问题是同时并发多次请求会被合并成为一次,导致协议被忽略,而并发协议其实是非常常见的功能。我们会使用创建 iframe 发送请求的方式。

(3) 通常考虑到安全性,需要在客户端中设置域名白名单或者限制,避免公司内部业务协议被第三方直接调用。

3.协议的拦截

客户端可以通过 API 对 WebView 发出的请求进行拦截:

  • IOS上: shouldStartLoadWithRequest
  • Android: shouldOverrideUrlLoading

当解析到请求 URL 头为制定的协议时,便不发起对应的资源请求,而是解析参数,并进行相关功能或者方法的调用,完成协议功能的映射。

4.协议回调

由于协议的本质其实是发送请求,这属于一个异步的过程,因此我们便需要处理对应的回调机制。这里我们采用的方式是JS的事件系统,这里我们会用到 window.addEventListenerwindow.dispatchEvent这两个基础API;

    1. 发送协议时,通过协议的唯一标识注册自定义事件,并将回调绑定到对应的事件上。
    1. 客户端完成对应的功能后,调用 Bridge 的dispatch API,直接携带 data 触发该协议的自定义事件。

图片描述

通过事件的机制,会让开发更符合我们前端的习惯,例如当你需要监听客户端的通知时,同样只需要在通过 addEventListener 进行监听即可。

Tips: 这里有一点需要注意的是,应该避免事件的多次重复绑定,因此当唯一标识重置时,需要removeEventListener对应的事件。

5.参数传递方式

由于 WebView 对 URL 会有长度的限制,因此常规的通过 search参数 进行传递的方式便具有一个问题,既 当需要传递的参数过长时,可能会导致被截断,例如传递base64或者传递大量数据时。

因此我们需要制定新的参数传递规则,我们使用的是函数调用的方式。这里的原理主要是基于:

Native 可以直接调用 JS 方法并直接获取函数的返回值。

我们只需要对每条协议标记一个唯一标识,并把参数存入参数池中,到时客户端再通过该唯一标识从参数池中获取对应的参数即可。

(二) Native 通知 Javascript

由于 Native 可以算作 H5 的宿主,因此拥有更大的权限,上面也提到了 Native 可以通过 WebView API直接执行 Js 代码。这样的权限也就让这个方向的通讯变得十分的便捷。

  • IOS: stringByEvaluatingJavaScriptFromString
// Swift
webview.stringByEvaluatingJavaScriptFromString("alert('NativeCall')")
  • Android: loadUrl (4.4-)
// 调用js中的JSBridge.trigger方法
// 该方法的弊端是无法获取函数返回值;
webView.loadUrl("javascript:JSBridge.trigger('NativeCall')")

Tips: 当系统低于4.4时,evaluateJavascript 是无法使用的,因此单纯的使用 loadUrl 无法获取 JS 返回值,这时我们需要使用前面提到的 prompt 的方法进行兼容,让 H5端 通过 prompt 进行数据的发送,客户端进行拦截并获取数据。

  • Android: evaluateJavascript (4.4+)
// 4.4+后使用该方法便可调用并获取函数返回值;
mWebView.evaluateJavascript("javascript:JSBridge.trigger('NativeCall')",      new ValueCallback<String>() {
    @Override
    public void onReceiveValue(String value) {
        //此处为 js 返回的结果
    }
});

基于上面的原理,我们已经明白 JSBridge 最基础的原理,并且能实现 Native <=> H5 的双向通讯机制了。

图片描述

(三) JSBridge 的接入

接下来,我们来理下代码上需要的资源。实现这套方案,从上图可以看出,其实可以分为两个部分:

  • JS部分(bridge): 在JS环境中注入 bridge 的实现代码,包含了协议的拼装/发送/参数池/回调池等一些基础功能。
  • Native部分(SDK):在客户端中 bridge 的功能映射代码,实现了URL拦截与解析/环境信息的注入/通用功能映射等功能。

我们这里的做法是,将这两部分一起封装成一个 Native SDK,由客户端统一引入。客户端在初始化一个 WebView 打开页面时,如果页面地址在白名单中,会直接在 HTML 的头部注入对应的 bridge.js。这样的做法有以下的好处:

  • 双方的代码统一维护,避免出现版本分裂的情况。有更新时,只要由客户端更新SDK即可,不会出现版本兼容的问题;
  • App的接入十分方便,只需要按文档接入最新版本的SDK,即可直接运行整套Hybrid方案,便于在多个App中快速的落地;
  • H5端无需关注,这样有利于将 bridge 开放给第三方页面使用。

这里有一点需要注意的是,协议的调用,一定是需要确保执行在bridge.js 成功注入后。由于客户端的注入行为属于一个附加的异步行为,从H5方很难去捕捉准确的完成时机,因此这里需要通过客户端监听页面完成后,基于上面的回调机制通知 H5端,页面中即可通过window.addEventListener('bridgeReady', e => {})进行初始化。

(四) App中 H5 的接入方式

将 H5 接入 App 中通常有两种方式:

(1) 在线H5,这是最常见的一种方式。我们只需要将H5代码部署到服务器上,只要把对应的 URL地址 给到客户端,用 WebView 打开该URL,即可嵌入。该方式的好处在于:

  • 独立性强,有非常独立的开发/调试/更新/上线能力;
  • 资源放在服务器上,完全不会影响客户端的包体积;
  • 接入成本很低,完全的热更新机制。

但相对的,这种方式也有对应的缺点:

  • 完全的网络依赖,在离线的情况下无法打开页面;
  • 首屏加载速度依赖于网络,网络较慢时,首屏加载也较慢;

通常,这种方式更适用在一些比较轻量级的页面上,例如一些帮助页、提示页、使用攻略等页面。这些页面的特点是功能性不强,不太需要复杂的功能协议,且不需要离线使用。在一些第三方页面接入上,也会使用这种方式,例如我们的页面调用微信JS-SDK。

(2) 内置包H5,这是一种本地化的嵌入方式,我们需要将代码进行打包后下发到客户端,并由客户端直接解压到本地储存中。通常我们运用在一些比较大和比较重要的模块上。其优点是:

  • 由于其本地化,首屏加载速度快,用户体验更为接近原生;
  • 可以不依赖网络,离线运行;

但同时,它的劣势也十分明显:

  • 开发流程/更新机制复杂化,需要客户端,甚至服务端的共同协作;
  • 会相应的增加 App 包体积;

这两种接入方式均有自己的优缺点,应该根据不同场景进行选择。

总结

本文主要解析了现在Hybrid App的发展现状和其基础原理,包含了

  • JavaScript 通知 Native
  • Native 通知 Javascript
  • JSBridge 的接入
  • H5 的接入

只有在了解了其最本质的实现原理后,才能对这套方案进行实现以及进一步的优化。接下来,我们将基于上面的理论,继续探讨如何把这套方案的真正代码实现以及方案优化方案,请继续 第二篇实战篇。欢迎大家一起讨论!更多文章内容请到github。感谢!😊

查看原文

libinfs 赞了文章 · 2018-09-24

Hybrid App技术解析 -- 原理篇

引言

随着 Web 技术和移动设备的快速发展,Hybrid 技术已经成为一种最主流最常见的方案。一套好的 Hybrid架构方案 能让 App 既能拥有极致的体验和性能,同时也能拥有 Web技术 灵活的开发模式、跨平台能力以及热更新机制,想想是不是都鸡冻不已。。😄。本系列文章是公司在这方面实践的一个总结,包含了原理解析、方案选型与实现、实践优化等方面。

大家可以到github上和我进行讨论哈!

第二篇实战篇 也已经完成了哈~~

现有混合方案

Hybrid App,俗称混合应用,即混合了 Native技术 与 Web技术 进行开发的移动应用。现在比较流行的混合方案主要有三种,主要是在UI渲染机制上的不同:

  1. 基于 WebView UI 的基础方案,市面上大部分主流 App 都有采用,例如微信JS-SDK,通过 JSBridge 完成 H5 与 Native 的双向通讯,从而赋予H5一定程度的原生能力。
  2. 基于 Native UI 的方案,例如 React-Native、Weex。在赋予 H5 原生API能力的基础上,进一步通过 JSBridge 将js解析成的虚拟节点树(Virtual DOM)传递到 Native 并使用原生渲染。
  3. 另外还有近期比较流行的小程序方案,也是通过更加定制化的 JSBridge,并使用双 WebView 双线程的模式隔离了JS逻辑与UI渲染,形成了特殊的开发模式,加强了 H5 与 Native 混合程度,提高了页面性能及开发体验。

以上的三种方案,其实同样都是基于 JSBridge 完成的通讯层,第二三种方案,其实可以看做是在方案一的基础上,继续通过不同的新技术进一步提高了应用的混合程度。因此,JSBridge 也是整个混合应用最关键的部分,例如我们在设置微信分享时用到的 JS-SDK,wx对象 便是我们最常见的 JSBridge:

图片描述

方案选型

任何技术方案的选型,其实都应该基于使用场景和现有条件。基于公司现有情况的几点考虑,在方案一上进一步优化,更加适合我们的需求。

  • 需求 Web技术 快速迭代、灵活开发的特点和线上热更新的机制。
  • 产品的核心能力是强大的拍照与底层图片处理能力,因此单纯的 H5技术能做的事非常有限,不能满足需求,通过 Hybrid 技术来强化H5,便是一种必需。
  • 公司业务上,并没有非常复杂的UI渲染需求,而且 App 中的一系列原生 UI组件 已经非常成熟,因此我们并不强需类似 RN 这样的方案。

因此,如何既能利用 H5 强大的开发和迭代能力,又能赋予 H5 强大的底层能力和用户体验,同时能复用现有的成熟 Native组件,便成为了我们最大的需求点 -- 一套完整又强大的 Hybrid技术架构方案。😠

Hybrid技术原理

Hybrid App的本质,其实是在原生的 App 中,使用 WebView 作为容器直接承载 Web页面。因此,最核心的点就是 Native端 与 H5端 之间的双向通讯层,其实这里也可以理解为我们需要一套跨语言通讯方案,来完成 Native(Java/Objective-c/...) 与 JavaScript 的通讯。这个方案就是我们所说的 JSBridge,而实现的关键,便是作为容器的 WebView,一切的原理都是基于 WebView 的机制。

图片描述

(一) JavaScript 通知 Native

基于 WebView 的机制和开放的 API, 实现这个功能有三种常见的方案:

  • API注入,原理其实就是 Native 获取 JavaScript环境上下文,并直接在上面挂载对象或者方法,使 js 可以直接调用,Android 与 IOS 分别拥有对应的挂载方式。
  • WebView 中的 prompt/console/alert 拦截,通常使用 prompt,因为这个方法在前端中使用频率低,比较不会出现冲突;
  • WebView URL Scheme 跳转拦截

第二三种机制的原理是类似的,都是通过对 WebView 信息冒泡传递的拦截,从而达到通讯的,接下来我们主要从 原理-定制协议-拦截协议-参数传递-回调机制 5个方面详细阐述下第三种方案 -- URL拦截方案。

1. 实现原理

在 WebView 中发出的网络请求,客户端都能进行监听和捕获

2. 协议的定制

我们需要制定一套URL Scheme规则,通常我们的请求会带有对应的协议开头,例如常见的 https://xxx.com 或者 file://1.jpg,代表着不同的含义。我们这里可以将协议类型的请求定制为:

xxcommand://xxxx?param1=1&param2=2

这里有几个需要注意点的是:

(1) xxcommand:// 只是一种规则,可以根据业务进行制定,使其具有含义,例如我们定义 xxcommand:// 为公司所有App系通用,为通用工具协议:

xxcommand://getProxy?h=1

而定义 xxapp:// 为每个App单独的业务协议。

xxapp://openCamera?h=2

不同的协议头代表着不同的含义,这样便能清楚知道每个协议的适用范围。

(2) 这里不要使用 location.href 发送,因为其自身机制有个问题是同时并发多次请求会被合并成为一次,导致协议被忽略,而并发协议其实是非常常见的功能。我们会使用创建 iframe 发送请求的方式。

(3) 通常考虑到安全性,需要在客户端中设置域名白名单或者限制,避免公司内部业务协议被第三方直接调用。

3.协议的拦截

客户端可以通过 API 对 WebView 发出的请求进行拦截:

  • IOS上: shouldStartLoadWithRequest
  • Android: shouldOverrideUrlLoading

当解析到请求 URL 头为制定的协议时,便不发起对应的资源请求,而是解析参数,并进行相关功能或者方法的调用,完成协议功能的映射。

4.协议回调

由于协议的本质其实是发送请求,这属于一个异步的过程,因此我们便需要处理对应的回调机制。这里我们采用的方式是JS的事件系统,这里我们会用到 window.addEventListenerwindow.dispatchEvent这两个基础API;

    1. 发送协议时,通过协议的唯一标识注册自定义事件,并将回调绑定到对应的事件上。
    1. 客户端完成对应的功能后,调用 Bridge 的dispatch API,直接携带 data 触发该协议的自定义事件。

图片描述

通过事件的机制,会让开发更符合我们前端的习惯,例如当你需要监听客户端的通知时,同样只需要在通过 addEventListener 进行监听即可。

Tips: 这里有一点需要注意的是,应该避免事件的多次重复绑定,因此当唯一标识重置时,需要removeEventListener对应的事件。

5.参数传递方式

由于 WebView 对 URL 会有长度的限制,因此常规的通过 search参数 进行传递的方式便具有一个问题,既 当需要传递的参数过长时,可能会导致被截断,例如传递base64或者传递大量数据时。

因此我们需要制定新的参数传递规则,我们使用的是函数调用的方式。这里的原理主要是基于:

Native 可以直接调用 JS 方法并直接获取函数的返回值。

我们只需要对每条协议标记一个唯一标识,并把参数存入参数池中,到时客户端再通过该唯一标识从参数池中获取对应的参数即可。

(二) Native 通知 Javascript

由于 Native 可以算作 H5 的宿主,因此拥有更大的权限,上面也提到了 Native 可以通过 WebView API直接执行 Js 代码。这样的权限也就让这个方向的通讯变得十分的便捷。

  • IOS: stringByEvaluatingJavaScriptFromString
// Swift
webview.stringByEvaluatingJavaScriptFromString("alert('NativeCall')")
  • Android: loadUrl (4.4-)
// 调用js中的JSBridge.trigger方法
// 该方法的弊端是无法获取函数返回值;
webView.loadUrl("javascript:JSBridge.trigger('NativeCall')")

Tips: 当系统低于4.4时,evaluateJavascript 是无法使用的,因此单纯的使用 loadUrl 无法获取 JS 返回值,这时我们需要使用前面提到的 prompt 的方法进行兼容,让 H5端 通过 prompt 进行数据的发送,客户端进行拦截并获取数据。

  • Android: evaluateJavascript (4.4+)
// 4.4+后使用该方法便可调用并获取函数返回值;
mWebView.evaluateJavascript("javascript:JSBridge.trigger('NativeCall')",      new ValueCallback<String>() {
    @Override
    public void onReceiveValue(String value) {
        //此处为 js 返回的结果
    }
});

基于上面的原理,我们已经明白 JSBridge 最基础的原理,并且能实现 Native <=> H5 的双向通讯机制了。

图片描述

(三) JSBridge 的接入

接下来,我们来理下代码上需要的资源。实现这套方案,从上图可以看出,其实可以分为两个部分:

  • JS部分(bridge): 在JS环境中注入 bridge 的实现代码,包含了协议的拼装/发送/参数池/回调池等一些基础功能。
  • Native部分(SDK):在客户端中 bridge 的功能映射代码,实现了URL拦截与解析/环境信息的注入/通用功能映射等功能。

我们这里的做法是,将这两部分一起封装成一个 Native SDK,由客户端统一引入。客户端在初始化一个 WebView 打开页面时,如果页面地址在白名单中,会直接在 HTML 的头部注入对应的 bridge.js。这样的做法有以下的好处:

  • 双方的代码统一维护,避免出现版本分裂的情况。有更新时,只要由客户端更新SDK即可,不会出现版本兼容的问题;
  • App的接入十分方便,只需要按文档接入最新版本的SDK,即可直接运行整套Hybrid方案,便于在多个App中快速的落地;
  • H5端无需关注,这样有利于将 bridge 开放给第三方页面使用。

这里有一点需要注意的是,协议的调用,一定是需要确保执行在bridge.js 成功注入后。由于客户端的注入行为属于一个附加的异步行为,从H5方很难去捕捉准确的完成时机,因此这里需要通过客户端监听页面完成后,基于上面的回调机制通知 H5端,页面中即可通过window.addEventListener('bridgeReady', e => {})进行初始化。

(四) App中 H5 的接入方式

将 H5 接入 App 中通常有两种方式:

(1) 在线H5,这是最常见的一种方式。我们只需要将H5代码部署到服务器上,只要把对应的 URL地址 给到客户端,用 WebView 打开该URL,即可嵌入。该方式的好处在于:

  • 独立性强,有非常独立的开发/调试/更新/上线能力;
  • 资源放在服务器上,完全不会影响客户端的包体积;
  • 接入成本很低,完全的热更新机制。

但相对的,这种方式也有对应的缺点:

  • 完全的网络依赖,在离线的情况下无法打开页面;
  • 首屏加载速度依赖于网络,网络较慢时,首屏加载也较慢;

通常,这种方式更适用在一些比较轻量级的页面上,例如一些帮助页、提示页、使用攻略等页面。这些页面的特点是功能性不强,不太需要复杂的功能协议,且不需要离线使用。在一些第三方页面接入上,也会使用这种方式,例如我们的页面调用微信JS-SDK。

(2) 内置包H5,这是一种本地化的嵌入方式,我们需要将代码进行打包后下发到客户端,并由客户端直接解压到本地储存中。通常我们运用在一些比较大和比较重要的模块上。其优点是:

  • 由于其本地化,首屏加载速度快,用户体验更为接近原生;
  • 可以不依赖网络,离线运行;

但同时,它的劣势也十分明显:

  • 开发流程/更新机制复杂化,需要客户端,甚至服务端的共同协作;
  • 会相应的增加 App 包体积;

这两种接入方式均有自己的优缺点,应该根据不同场景进行选择。

总结

本文主要解析了现在Hybrid App的发展现状和其基础原理,包含了

  • JavaScript 通知 Native
  • Native 通知 Javascript
  • JSBridge 的接入
  • H5 的接入

只有在了解了其最本质的实现原理后,才能对这套方案进行实现以及进一步的优化。接下来,我们将基于上面的理论,继续探讨如何把这套方案的真正代码实现以及方案优化方案,请继续 第二篇实战篇。欢迎大家一起讨论!更多文章内容请到github。感谢!😊

查看原文

赞 173 收藏 137 评论 21

libinfs 赞了文章 · 2018-08-03

由throw new Error() 引发的探讨

问题复现

在工作时遇到了需要抛出异常并且需要自己捕获处理的地方,于是在抛出的地方写下

function parseExcel(con) {
   try {
        // doSomething
    } catch (error) {
        throw new Error('parse excel failed');
    }
}

在捕获的地方写下:

try {
    parseExcel(con);
} catch (error) {
    if (error === 'parse excel failed') {
        //doSomething
    }
}

当时自己感觉妥妥的,没毛病。
后来当其他地方出现了诡异的bug,定位问题时才发现这里的写法严重不对。

问题分析

可以肯定问题出在异常捕获而不是抛出。
这里是使用了throw来抛出异常,并且还是使用

throw new Error('error message');

这样的实例化写法,这样的写法是很规范的,是js规范所推崇的。但是这里需要注意的是,throw出去的是一个Error对象,而类似下面的这种字符串

throw 'error message'; // 不建议的写法

所以捕获的时候捕获到的也是一个对象,这样一个对象与parse excel failed字符串比较显然是不正确的。
那么该怎么去捕获这个error message呢?
通过对《JavaScript高级程序设计 (第三版) 》对于抛出错误的学习我们可以了解到:

在抛出的Error对象中有一个被广泛支持的属性:namemessage
name:用来存储错误的类型,在ECMA-262定义了七种错误类型:ErrorEvalErrorRangeErrorReferenceErrorSyntaxErrorTypeErrorURIError。详情见:try-catch语句
message:用来存储error message,就是你new Error()时候传进去的参数
到此,上面的问题应迎刃而解。

问题解决

由于是异常捕获时候的错误所以我们在捕获的时候这样处理

try {
    parseExcel(con);
} catch (error) {
    if (error.message === 'parse excel failed') {
        //doSomething
    }
}

至此,问题解决。

反思

我在这之前是从未试用过throw的,在用的时候也是从不深究,马马虎虎拿来就用,如果不是因为后来出了问题去解决,估计不会发现原来js的错误处理还有很多道道。写程序是一件很严谨的事情,一丝一毫也马虎不得,更不可想当然。

查看原文

赞 3 收藏 1 评论 3

认证与成就

  • 获得 251 次点赞
  • 获得 4 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 4 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-05-28
个人主页被 1.6k 人浏览