Margin

Margin 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

Margin 收藏了文章 · 9月30日

深入理解Chrome V8垃圾回收机制

最近,项目进入维护期,基本没有什么需求,比较闲,这让我莫名的有了危机感,每天像是在混日子,感觉这像是在温水煮青蛙,已经毕业3年了,很怕自己到了5年经验的时候,能力却和3年经验的时候一样,没什么长进。于是开始整理自己的技术点,刚好查漏补缺,在收藏夹在翻出了一篇文章一名【合格】前端工程师的自检清单,看到了里面的两个问题:

  • JavaScript中的变量在内存中的具体存储形式是什么?
  • 浏览器的垃圾回收机制,如何避免内存泄漏?

然后各种查资料,就整理了这篇文章。

阅读本文之后,你可以了解到:

  • JavaScript的内存是怎么管理的?
  • Chrome是如何进行垃圾回收的?
  • Chrome对垃圾回收进行了哪些优化?

原文地址 欢迎star

JavaScript的内存管理

不管什么程序语言,内存生命周期基本是一致的:

  1. 分配你所需要的内存
  2. 使用分配到的内存(读、写)
  3. 不需要时将其释放归还

与其他需要手动管理内存的语言不通,在JavaScript中,当我们创建变量(对象,字符串等)的时候,系统会自动给对象分配对应的内存。

var n = 123; // 给数值变量分配内存
var s = "azerty"; // 给字符串分配内存

var o = {
  a: 1,
  b: null
}; // 给对象及其包含的值分配内存

// 给数组及其包含的值分配内存(就像对象一样)
var a = [1, null, "abra"]; 

function f(a){
  return a + 2;
} // 给函数(可调用的对象)分配内存

// 函数表达式也能分配一个对象
someElement.addEventListener('click', function(){
  someElement.style.backgroundColor = 'blue';
}, false);

当系统发现这些变量不再被使用的时候,会自动释放(垃圾回收)这些变量的内存,开发者不用过多的关心内存问题。

虽然这样,我们开发过程中也需要了解JavaScript的内存管理机制,这样才能避免一些不必要的问题,比如下面代码:

{}=={} // false
[]==[] // false
''=='' // true

在JavaScript中,数据类型分为两类,简单类型和引用类型,对于简单类型,内存是保存在栈(stack)空间中,复杂数据类型,内存是保存在堆(heap)空间中。

  • 基本类型:这些类型在内存中分别占有固定大小的空间,他们的值保存在栈空间,我们通过按值来访问的
  • 引用类型:引用类型,值大小不固定,栈内存中存放地址指向堆内存中的对象。是按引用访问的。

而对于栈的内存空间,只保存简单数据类型的内存,由操作系统自动分配和自动释放。而堆空间中的内存,由于大小不固定,系统无法无法进行自动释放,这个时候就需要JS引擎来手动的释放这些内存。

为什么需要垃圾回收

在Chrome中,v8被限制了内存的使用(64位约1.4G/1464MB , 32位约0.7G/732MB),为什么要限制呢?

  1. 表层原因是,V8最初为浏览器而设计,不太可能遇到用大量内存的场景
  2. 深层原因是,V8的垃圾回收机制的限制(如果清理大量的内存垃圾是很耗时间,这样回引起JavaScript线程暂停执行的时间,那么性能和应用直线下降)

前面说到栈内的内存,操作系统会自动进行内存分配和内存释放,而堆中的内存,由JS引擎(如Chrome的V8)手动进行释放,当我们的代码没有按照正确的写法时,会使得JS引擎的垃圾回收机制无法正确的对内存进行释放(内存泄露),从而使得浏览器占用的内存不断增加,进而导致JavaScript和应用、操作系统性能下降。

Chrome 垃圾回收算法

在JavaScript中,其实绝大多数的对象存活周期都很短,大部分在经过一次的垃圾回收之后,内存就会被释放掉,而少部分的对象存活周期将会很长,一直是活跃的对象,不需要被回收。为了提高回收效率,V8 将堆分为两类新生代老生代,新生代中存放的是生存时间短的对象,老生代中存放的生存时间久的对象。

新生区通常只支持 1~8M 的容量,而老生区支持的容量就大很多了。对于这两块区域,V8 分别使用两个不同的垃圾回收器,以便更高效地实施垃圾回收。

  • 副垃圾回收器 - Scavenge:主要负责新生代的垃圾回收。
  • 主垃圾回收器 - Mark-Sweep & Mark-Compact:主要负责老生代的垃圾回收。

新生代垃圾回收器 - Scavenge

在JavaScript中,任何对象的声明分配到的内存,将会先被放置在新生代中,而因为大部分对象在内存中存活的周期很短,所以需要一个效率非常高的算法。在新生代中,主要使用Scavenge算法进行垃圾回收,Scavenge算法是一个典型的牺牲空间换取时间的复制算法,在占用空间不大的场景上非常适用。

Scavange算法将新生代堆分为两部分,分别叫from-spaceto-space,工作方式也很简单,就是将from-space中存活的活动对象复制到to-space中,并将这些对象的内存有序的排列起来,然后将from-space中的非活动对象的内存进行释放,完成之后,将from spaceto space进行互换,这样可以使得新生代中的这两块区域可以重复利用。

简单的描述就是:

  • 标记活动对象和非活动对象
  • 复制 from space 的活动对象到 to space 并对其进行排序
  • 释放 from space 中的非活动对象的内存
  • 将 from space 和 to space 角色互换

那么,垃圾回收器是怎么知道哪些对象是活动对象和非活动对象的呢?

有一个概念叫对象的可达性,表示从初始的根对象(window,global)的指针开始,这个根指针对象被称为根集(root set),从这个根集向下搜索其子节点,被搜索到的子节点说明该节点的引用对象可达,并为其留下标记,然后递归这个搜索的过程,直到所有子节点都被遍历结束,那么没有被标记的对象节点,说明该对象没有被任何地方引用,可以证明这是一个需要被释放内存的对象,可以被垃圾回收器回收。

新生代中的对象什么时候变成老生代的对象呢?

在新生代中,还进一步进行了细分,分为nursery子代和intermediate子代两个区域,一个对象第一次分配内存时会被分配到新生代中的nursery子代,如果进过下一次垃圾回收这个对象还存在新生代中,这时候我们移动到 intermediate 子代,再经过下一次垃圾回收,如果这个对象还在新生代中,副垃圾回收器会将该对象移动到老生代中,这个移动的过程被称为晋升。

老生代垃圾回收 - Mark-Sweep & Mark-Compact

新生代空间中的对象满足一定条件后,晋升到老生代空间中,在老生代空间中的对象都已经至少经历过一次或者多次的回收所以它们的存活概率会更大,如果这个时候再使用scavenge算法的话,会出现两个问题:

  • scavenge为复制算法,重复复制活动对象会使得效率低下
  • scavenge是牺牲空间来换取时间效率的算法,而老生代支持的容量较大,会出现空间资源浪费问题

所以在老生代空间中采用了 Mark-Sweep(标记清除) 和 Mark-Compact(标记整理) 算法。

Mark-Sweep

Mark-Sweep处理时分为两阶段,标记阶段和清理阶段,看起来与Scavenge类似,不同的是,Scavenge算法是复制活动对象,而由于在老生代中活动对象占大多数,所以Mark-Sweep在标记了活动对象和非活动对象之后,直接把非活动对象清除。

  • 标记阶段:对老生代进行第一次扫描,标记活动对象
  • 清理阶段:对老生代进行第二次扫描,清除未被标记的对象,即清理非活动对象

看似一切 perfect,但是还遗留一个问题,被清除的对象遍布于各内存地址,产生很多内存碎片。

Mark-Compact

由于Mark-Sweep完成之后,老生代的内存中产生了很多内存碎片,若不清理这些内存碎片,如果出现需要分配一个大对象的时候,这时所有的碎片空间都完全无法完成分配,就会提前触发垃圾回收,而这次回收其实不是必要的。

为了解决内存碎片问题,Mark-Compact被提出,它是在 Mark-Sweep的基础上演进而来的,相比Mark-Sweep,Mark-Compact添加了活动对象整理阶段,将所有的活动对象往一端移动,移动完成后,直接清理掉边界外的内存。

全停顿 Stop-The-World

由于垃圾回收是在JS引擎中进行的,而Mark-Compact算法在执行过程中需要移动对象,而当活动对象较多的时候,它的执行速度不可能很快,为了避免JavaScript应用逻辑和垃圾回收器的内存资源竞争导致的不一致性问题,垃圾回收器会将JavaScript应用暂停,这个过程,被称为全停顿(stop-the-world)。

在新生代中,由于空间小、存活对象较少、Scavenge算法执行效率较快,所以全停顿的影响并不大。而老生代中就不一样,如果老生代中的活动对象较多,垃圾回收器就会暂停主线程较长的时间,使得页面变得卡顿。

优化 Orinoco

orinoco为V8的垃圾回收器的项目代号,为了提升用户体验,解决全停顿问题,它利用了增量标记、懒性清理、并发、并行来降低主线程挂起的时间。

增量标记 - Incremental marking

为了降低全堆垃圾回收的停顿时间,增量标记将原本的标记全堆对象拆分为一个一个任务,让其穿插在JavaScript应用逻辑之间执行,它允许堆的标记时的5~10ms的停顿。增量标记在堆的大小达到一定的阈值时启用,启用之后每当一定量的内存分配后,脚本的执行就会停顿并进行一次增量标记。

懒性清理 - Lazy sweeping

增量标记只是对活动对象和非活动对象进行标记,惰性清理用来真正的清理释放内存。当增量标记完成后,假如当前的可用内存足以让我们快速的执行代码,其实我们是没必要立即清理内存的,可以将清理的过程延迟一下,让JavaScript逻辑代码先执行,也无需一次性清理完所有非活动对象内存,垃圾回收器会按需逐一进行清理,直到所有的页都清理完毕。

增量标记与惰性清理的出现,使得主线程的最大停顿时间减少了80%,让用户与浏览器交互过程变得流畅了许多,从实现机制上,由于每个小的增量标价之间执行了JavaScript代码,堆中的对象指针可能发生了变化,需要使用写屏障技术来记录这些引用关系的变化,所以也暴露出来增量标记的缺点:

  • 并没有减少主线程的总暂停的时间,甚至会略微增加
  • 由于写屏障(Write-barrier)机制的成本,增量标记可能会降低应用程序的吞吐量

并发 - Concurrent

并发式GC允许在在垃圾回收的同时不需要将主线程挂起,两者可以同时进行,只有在个别时候需要短暂停下来让垃圾回收器做一些特殊的操作。但是这种方式也要面对增量回收的问题,就是在垃圾回收过程中,由于JavaScript代码在执行,堆中的对象的引用关系随时可能会变化,所以也要进行写屏障操作。

并行 - Parallel

并行式GC允许主线程和辅助线程同时执行同样的GC工作,这样可以让辅助线程来分担主线程的GC工作,使得垃圾回收所耗费的时间等于总时间除以参与的线程数量(加上一些同步开销)。

V8当前垃圾回收机制

2011年,V8应用了增量标记机制。直至2018年,Chrome64和Node.js V10启动并发标记(Concurrent),同时在并发的基础上添加并行(Parallel)技术,使得垃圾回收时间大幅度缩短。

副垃圾回收器

V8在新生代垃圾回收中,使用并行(parallel)机制,在整理排序阶段,也就是将活动对象从from-to复制到space-to的时候,启用多个辅助线程,并行的进行整理。由于多个线程竞争一个新生代的堆的内存资源,可能出现有某个活动对象被多个线程进行复制操作的问题,为了解决这个问题,V8在第一个线程对活动对象进行复制并且复制完成后,都必须去维护复制这个活动对象后的指针转发地址,以便于其他协助线程可以找到该活动对象后可以判断该活动对象是否已被复制。

主垃圾回收器

V8在老生代垃圾回收中,如果堆中的内存大小超过某个阈值之后,会启用并发(Concurrent)标记任务。每个辅助线程都会去追踪每个标记到的对象的指针以及对这个对象的引用,而在JavaScript代码执行时候,并发标记也在后台的辅助进程中进行,当堆中的某个对象指针被JavaScript代码修改的时候,写入屏障(write barriers)技术会在辅助线程在进行并发标记的时候进行追踪。

当并发标记完成或者动态分配的内存到达极限的时候,主线程会执行最终的快速标记步骤,这个时候主线程会挂起,主线程会再一次的扫描根集以确保所有的对象都完成了标记,由于辅助线程已经标记过活动对象,主线程的本次扫描只是进行check操作,确认完成之后,某些辅助线程会进行清理内存操作,某些辅助进程会进行内存整理操作,由于都是并发的,并不会影响主线程JavaScript代码的执行。

结束

其实,大部分JavaScript开发人员并不需要考虑垃圾回收,但是了解一些垃圾回收的内部原理,可以帮助你了解内存的使用情况,根据内存使用观察是否存在内存泄露,而防止内存泄露,是提升应用性能的一个重要举措。

参考文献

查看原文

Margin 收藏了文章 · 9月18日

透视HTTPS建造固若金汤的城堡

公众号:码哥字节,转载请联系公众号

为什么有 HTTPS?因为 HTTP 不安全! 现在的互联网已经不再是 “田园时代”,“黑暗森林” 已经到来。上网的记录会被轻易截获,网站是否真实也无法验证,黑客可以伪装成银行网站,盗取真实姓名、密码、银行卡等敏感信息,威胁人身安全和财产安全。

上网的时候必须步步为营、处处小心,否则就会被不知道埋伏在哪里的黑客所“猎杀”。

HTTPS 如何实现安全通信?如何构建出固若金汤的网络城堡?主要涉及的知识点如下:

  • 了解什么是 HTTPS
  • 什么样的才是安全的通信
  • 对称加密与非对称加密、摘要算法、数字签名、完整性校验到底是什么
  • 迁移 HTTPS 的必要性

什么是安全

做事要稳,老司机【码哥字节】开车要安全!不管是戴杜蕾斯还是安全气囊,“安全至关重要”!

在通信过程中,具备以下特性则认为安全:机密性、完整性、不可否认、身份认证

机密性

数据必须保密,只能有信任的人读取,其他人是不可见的秘密。诸葛亮的密报总不能让司马懿知道呀,不然还玩个蛋。通俗的说:就是不能让不相关的人看到不该看的东西。

完整性

也叫作一致性,也就是数据在传输过程中没有被非法篡改,内容不能多也不能少,一五一十的保持原状。

打个比方,原本张无忌说:“赵敏,么么哒。”,传信的飞鸽被周芷若抓到了,截取了消息,改成了 “赵敏,去死吧!”。这么子搞,倚天屠龙记可能就会被改写了。

不可否认

也就做不可抵赖,不能否认已经发生过的事情。所谓 “君子一言,驷马难追”。“老懒” 这种事情不能发生。

就像尹志平亲密接触了小龙女,事后一直隐瞒否认,装作不知道,这是万万不可的。所以最终就嗝屁了。

身份验证

也就是确认对方的真实身份,“证明你是真的是你”,保证消息发送到可信的人,而不是非法之徒。

比如令狐冲写了一份情书给任盈盈:“盈盈,冲哥哥爱你哟”,但是岳不群看到快递小哥,冒充是令狐冲,截取了情书后回复:“傻逼,白日做梦”。令狐冲不知道这是岳不群的回复,以为是任盈盈的,笑傲江湖又要重写了……

所以同时具备了机密性、完整性、身份认证、不可够人四个特性,通信双方的安全才有保证,才是真正的安全。

什么是 HTTPS

到这里,终于轮到 HTTPS 上台了,也就是它为 HTTP 增加了刚刚说的四大安全特性。

HTTPS 其实是一个“非常简单”的协议,规定了新的协议名“https”,默认端口号 443,至于其他的什么请求 - 应答模式、报文结构、请求方法、URI、头字段、连接管理等等都完全沿用 HTTP,没有任何新的东西。唯一的差别就是端口号不同、去掉明文传输。

那 HTTPS 凭啥就变得安全了呢?

就是因为他在 TCP/IP 与 HTTP 之间加上了 SSL/TLS ,从原来的 HTTP over TCP/IP 变成了 HTTP over SSL/TLS,让 HTTP 运行在 安全的 SSL/TLS 协议上,安全开车。

http与https

所以重点就是去掌握 SSL/TLS 到底是什么玩意成就了安全。

SSL/TLS

SSL 即安全套接层(Secure Sockets Layer),在 OSI 模型中处于第 5 层(会话层),由网景公司于 1994 年发明,有 v2 和 v3 两个版本,而 v1 因为有严重的缺陷从未公开过。

SSL 发展到 v3 时已经证明了它自身是一个非常好的安全通信协议,于是互联网工程组 IETF 在 1999 年把它改名为 TLS(传输层安全,Transport Layer Security),正式标准化,版本号从 1.0 重新算起,所以 TLS1.0 实际上就是 SSLv3.1。

TLS 由记录协议、握手协议、警告协议、变更密码规范协议、扩展协议等几个子协议组成,综合使用了对称加密、非对称加密、身份认证等许多密码学前沿技术。

浏览器与服务器在使用 TLS 建立连接的时候实际上就是选了一组加密算法实现安全通信,这些算法组合叫做 “密码套件(cipher suite)”。

套件命名很有规律,比如“ECDHE-RSA-AES256-GCM-SHA384”。按照 密钥交换算法 + 签名算法 + 对称加密算法 + 摘要算法”组成的.

所以这个套件的意思就是:使用 ECDHE 算法进行密钥交换,使用 RSA 签名和身份验证,握手后使用 AES 对称加密,密钥长度 256 位,分组模式 GCM,消息认证和随机数生成使用摘要算法 SHA384。

对称加密与非对称加密

前面提到四个实现安全的必要条件,先说 机密性,也就是消息只能给想给的人看到并且看得懂。

实现机密性的手段就是 加密(encrypt),也就是将原本明文消息使用加密算法转换成别人看不懂的密文,只有掌握特有的 密钥 的人才能解密出原始内容。就好像是诸葛亮将发给关二爷密报的内容通过一种转换算法转成其他的内容,司马懿看不懂。关二爷持有解密该内容的关键钥匙。

钥匙也就是 密钥(key),未加密的消息叫做 明文 (plain text/clear text),加密后的内容叫做 密文(cipher text),通过密钥解密出原文的过程叫做 解密(decrypt),而加解密的整个过程就是 加密算法

由于 HTTPS、TLS 都运行在计算机上,所以“密钥”就是一长串的数字,但约定俗成的度量单位是“位”(bit),而不是“字节”(byte)。比如,说密钥长度是 128,就是 16 字节的二进制串,密钥长度 1024,就是 128 字节的二进制串。

加密算法通常有两大类:对称加密和非对称加密。

对称加密

加密和解密使用的密钥都是同一个,是 “对称的”。双方只要保证不会有泄露其他人知道这个密钥,通信就具有机密性。

对称加密算法常见的有 RC4、DES、3DES、AES、ChaCha20 等,但前三种算法都被认为是不安全的,通常都禁止使用,目前常用的只有 AES 和 ChaCha20。

AES 的意思是“高级加密标准”(Advanced Encryption Standard),密钥长度可以是 128、192 或 256。它是 DES 算法的替代者,安全强度很高,性能也很好,而且有的硬件还会做特殊优化,所以非常流行,是应用最广泛的对称加密算法。

加密分组模式

对称算法还有一个 “分组模式”的概念,目的是通过算法用固定长度的密钥加密任意长度的明文。

最新的分组模式被称为 AEAD(Authenticated Encryption with Associated Data),在加密的同时增加了认证的功能,常用的是 GCM、CCM 和 Poly1305。

非对称加密

有对称加密,为何还搞出一个非对称加密呢?

对称加密确实解决了机密性,只有相关的人才能读取出信息。但是最大的问题是:如何安全的把密钥传递对方,专业术语 “密钥交换”。

这个很容易理解,对称加密的密钥在飞鸽传书过程中被打鸟的敌军捕获窃取,那么就能随意加解密收发作战密报数据了,诸葛亮的密报没有机密可言。

所以非对称加密诞生了。

由两个密钥组成,分别是 公钥(public key) 和 “私钥(private key)”,两个密钥是不一样的,这也就是不对称的由来,公钥可以任何人使用,私钥则自己保密。

这里需要注意的是:公钥和私钥都可以用来加密解密,公钥加密的密文只能用私钥解密,反之亦然。

服务端保存私钥,在互联网上分发公钥,当访问服务器网站的时候使用授予的公钥加密明文即可,服务端则使用对应的私钥来解密。敌军没有私钥也就无法破解密文了。

非对称加密

TLS 中常见的加密算法有 DH、RSA、ECC、DSA 等。其中的 RSA 最常用,它的安全性基于“整数分解”的数学难题,使用两个超大素数的乘积作为生成密钥的材料,想要从公钥推算出私钥是非常困难的。

ECC(Elliptic Curve Cryptography)是非对称加密里的“后起之秀”,它基于“椭圆曲线离散对数”的数学难题,使用特定的曲线方程和基点生成公钥和私钥,子算法 ECDHE 用于密钥交换,ECDSA 用于数字签名。

比起 RSA,ECC 在安全强度和性能上都有明显的优势。160 位的 ECC 相当于 1024 位的 RSA,而 224 位的 ECC 则相当于 2048 位的 RSA。因为密钥短,所以相应的计算量、消耗的内存和带宽也就少,加密解密的性能就上去了,对于现在的移动互联网非常有吸引力。

现在我们为了机密性从对称加密到非对称加密,而非对称加密还解决了密钥交换不安全的问题。那么是否可以直接使用非对称加密来实现机密性呢?

答案是否定的!

因为非对称加密运算速度比较慢。所以需要两者结合,混合模式实现机密性问题,同时又有很好的性能。

加密流程如下所示

  1. 先创建一个随机数的对称加密密钥,会话密钥(session key)
  2. 使用会话密钥加密需要传输的明文消息,因为对称加密性能较好,接着再使用非对称加密的公钥对会话密钥加密,因为会话密钥很短,通常只有 16 字节或 32 字节,所以加密也不会太慢。这里主要就是解决了非对称加密的性能问题,同时实现了会话密钥的机密交换。
  3. 另一方接收到密文后使用非对称加密的私钥解密出上一步加密的 会话密钥,接着使用会话密钥解密出加密的消息明文。

混合加密

总结一下就是使用非对称加密算法来加密会话密钥,使用对称加密算法来加密消息明文,接收方则使用非对称加密算法的私钥解密出会话密钥,再利用会话密钥解密消息密文。

这样混合加密就解决了对称加密算法的密钥交换问题,而且安全和性能兼顾,完美地实现了机密性。

后面还有完整性、身份认证、不可否认等特性没有实现,所以现在的通信还不是绝对安全。

摘要算法与完整性

摘要算法的主要目的就是实现完整性,通过常见的散列函数、哈希函数实现。

我们可以简单理解成这事一种特殊的压缩算法,将任意长度的明文数据处理成固定长度、又是独一无二的“摘要”字符串,就是该数据的指纹。

同时摘要算法是单向加密算法,没有密钥,加密后的数据也无法解密,也就是不能从“摘要”推导出明文。

比如我们听过或者用过的 MD5(Message-Digest 5)SHA-1(Secure Hash Algorithm 1),它们就是最常用的两个摘要算法,能够生成 16 字节和 20 字节长度的数字摘要。

完整性实现

有了摘要算法生成的数字摘要,那么我们只需要在明文数据附上对应的摘要,就能保证数据的完整性。

但是由于摘要算法不具有机密性,不能明文传输,否则黑客可以修改消息后把摘要也一起改了,网站还是鉴别不出完整性。

所以完整性还是要建立在机密性上,我们结合之前提到的混合加密使用 ”会话密钥“ 加密明文消息 + 摘要,这样的话黑客也就无法得到明文,无法做修改了。这里有个专业术语叫“哈希消息认证码(HMAC)”。

哈希消息认证码(HMAC)

比如诸葛亮使用上面提到的混合加密过程给关二爷发消息:“明天攻城” + “SHA-2 摘要”,关二爷收到后使用密钥将解密出来的会话密钥解密出明文消息,同时对明文消息使用解密出来的摘要算法进行摘要计算,接着比对两份“摘要”字符串是否一致,如果一致就说明消息完整可信,没有被敌军修改过。

消息被修改是很危险的,要以史为鉴,比如赵高与李斯伪造遗诏,直接把扶苏给送西天了,这太可怕了。

总结下就是通过摘要比对防止篡改,同时利用混合加密实现密文与摘要的安全传输。

数字签名和 CA

到这里已经很安全了,但是还是有漏洞,就是通信的两头。黑客可以伪装成网站来窃取信息。而反过来,他也可以伪装成你,向网站发送支付、转账等消息,网站没有办法确认你的身份,钱可能就这么被偷走了。

现在如何实现身份认证呢?

现实生活中,解决身份认证的手段是签名和印章,只要在纸上写下签名或者盖个章,就能够证明这份文件确实是由本人而不是其他人发出的。

非对称加密依然可以解决此问题,只不过跟之前反过来用,使用私钥再加上摘要算法,就能够实现“数字签名”,同时实现“身份认证”和“不可否认”。

就是把公钥私钥的用法反过来,之前是公钥加密、私钥解密,现在是私钥加密、公钥解密。但又因为非对称加密效率太低,所以私钥只加密原文的摘要,这样运算量就小的多,而且得到的数字签名也很小,方便保管和传输。

重点就是使用非对称加密的“私钥”加密原文的摘要,对方则使用非对称加密的公钥解密出摘要,再比对解密出的原文通过摘要算法计算摘要与解密出的摘要比对是否一致。这样就能像签署文件一样证明消息确实是你发送的。

签名验签

只要你和网站互相交换公钥,就可以用“签名”和“验签”来确认消息的真实性,因为私钥保密,黑客不能伪造签名,就能够保证通信双方的身份。

CA

到这里似乎已经大功告成,可惜还不是。

综合使用对称加密、非对称加密和摘要算法,我们已经实现了安全的四大特性,是不是已经完美了呢?

不是的,这里还有一个“公钥的信任”问题。因为谁都可以发布公钥,我们还缺少防止黑客伪造公钥的手段,也就是说,怎么来判断这个公钥就是你或者张三丰的公钥呢?

这个“第三方”就是我们常说的CA(Certificate Authority,证书认证机构)。它就像网络世界里的公安局、教育部、公证中心,具有极高的可信度,由它来给各个公钥签名,用自身的信誉来保证公钥无法伪造,是可信的。

CA 对公钥的签名认证也是有格式的,不是简单地把公钥绑定在持有者身份上就完事了,还要包含序列号、用途、颁发者、有效时间等等,把这些打成一个包再签名,完整地证明公钥关联的各种信息,形成“数字证书”(Certificate)。

OpenSSL

它是一个著名的开源密码学程序库和工具包,几乎支持所有公开的加密算法和协议,已经成为了事实上的标准,许多应用软件都会使用它作为底层库来实现 TLS 功能,包括常用的 Web 服务器 Apache、Nginx 等。

由于 OpenSSL 是开源的,所以它还有一些代码分支,比如 Google 的 BoringSSL、OpenBSD 的 LibreSSL,这些分支在 OpenSSL 的基础上删除了一些老旧代码,也增加了一些新特性,虽然背后有“大金主”,但离取代 OpenSSL 还差得很远。

总结下就是:OpenSSL 是著名的开源密码学工具包,是 SSL/TLS 的具体实现。

迁移 HTTPS 必要性

如果你做移动应用开发的话,那么就一定知道,Apple、Android、某信等开发平台在 2017 年就相继发出通知,要求所有的应用必须使用 HTTPS 连接,禁止不安全的 HTTP。

在台式机上,主流的浏览器 Chrome、Firefox 等也早就开始“强推”HTTPS,把 HTTP 站点打上“不安全”的标签,给用户以“心理压力”。

Google 等搜索巨头还利用自身的“话语权”优势,降低 HTTP 站点的排名,而给 HTTPS 更大的权重,力图让网民只访问到 HTTPS 网站。

这些手段都逐渐“挤压”了纯明文 HTTP 的生存空间,“迁移到 HTTPS”已经不是“要不要做”的问题,而是“要怎么做”的问题了。HTTPS 的大潮无法阻挡,如果还是死守着 HTTP,那么无疑会被冲刷到互联网的角落里。

顾虑

阻碍 HTTPS 实施的因素还有一些这样、那样的顾虑,我总结出了三个比较流行的观点:“慢、贵、难”。

而“慢”则是惯性思维,拿以前的数据来评估 HTTPS 的性能,认为 HTTPS 会增加服务器的成本,增加客户端的时延,影响用户体验。

其实现在服务器和客户端的运算能力都已经有了很大的提升,性能方面完全没有担心的必要,而且还可以应用很多的优化解决方案

所谓“贵”,主要是指证书申请和维护的成本太高,网站难以承担。

这也属于惯性思维,在早几年的确是个问题,向 CA 申请证书的过程不仅麻烦,而且价格昂贵,每年要交几千甚至几万元。

但现在就不一样了,为了推广 HTTPS,很多云服务厂商都提供了一键申请、价格低廉的证书,而且还出现了专门颁发免费证书的 CA,其中最著名的就是“Let’s Encrypt”。

所谓的“难”,是指 HTTPS 涉及的知识点太多、太复杂,有一定的技术门槛,不能很快上手。

总结

从什么是安全我们延展出 HTTPS,解释了什么是 HTTPS,以及与 HTTP 的区别。HTTPS 主要就是通过 SSL/TLS 实现安全,而安全我们又接触了什么是对称加密与非对称加密,非对称加密性能较弱,所以我们使用非对称加密来加密对称加密的“会话密钥”,利用会话密钥加密明文解决了性能问题。

通过混合加密实现了机密性,利用摘要算法实现了完整性,通过数字签名使用非对称加密的“私钥”加密原文的摘要,对方则使用非对称加密的公钥解密出摘要,再比对解密出的原文通过摘要算法计算摘要与解密出的摘要比对是否一致实现了身份认证与不可否认。

如果觉得阅读后对你有帮助,希望多多分享、点赞与在看素质三连不做白嫖者。

关注 【码哥字节】解锁更多硬核。

推荐阅读

以下几篇文章阅读量与读者反馈都很好,推荐大家阅读:

公众号后台回复 ”加群“,加入读者技术群,里面有阿里、腾讯的小伙伴一起探讨技术。

MageByte

参考内容

[透视 HTTP 协议]

查看原文

Margin 提出了问题 · 9月18日

请问大家H5的移动端顶栏是怎么实现动态内容?

绝大多数页面都有顶栏,而且大多数页面的顶栏内容都不一样。
顶栏内容通常都是:左侧返回或关闭,中间标题或下拉,右侧有+号按钮,有更多按钮,有关闭按钮。

顶栏这部分是做成了公共组件,然后每个页面去写标签吗?还是用动态组件?
image.png

关注 3 回答 2

Margin 提出了问题 · 9月16日

如果一个promise对象永远pending会有什么后果?

如果一个promise对象一直不被resolve或reject,在其then上注册回调就一直不会被调用,该回调产生的闭包是否会一直留在内存中,不会被回收,从而导致内存泄漏?

使用场景如下:

// request.js
// 请重点关注axios请求出错的拦截器
import axios from 'axios'

const request = axios.create()
request.interceptors.response
    .use(
        response => response.data,
        error => {
            if (error.response.status === 401) {
                // 该promise对象一直处于pending
                window.alert('登录信息失效!')
                return new Promise(() => {})
            }
        }
 )

export default request
// Userinfo.vue
<script>
import request from '../request.js'

export default {
    data() {
        return {
            loading: false,
            users: []
        }
    },
    methods: {
        getUserList(id) {
            this.loading = true
            return request.get('/users')
                .then(res => {
                    this.loading = false
                    this.users = res
                })
                .catch(e => {
                    this.loading = false
                    this.$notify.error(e.message)
                })
        }
    }
}
</script>

说明:拦截器拦截到401的错误后,返回了一个promise对象,该promise实例化时传入的回调既不会调用reject,也不会调用resolve,是永远处于pending状态的。

之所以不resolve或reject是不想走axios调用者的catch,防止重复多余的提示(拦截器里一次,catch里一次)。

请问这样的写法,then和catch由于有vue实例的闭包引用,在页面被销毁之后,vue实例会被回收吗?

关注 2 回答 2

Margin 赞了文章 · 8月21日

学习Vue3.0,先来了解一下Proxy

产品经理身旁过,需求变更逃不过。
测试姐姐眯眼笑,今晚bug必然多。

据悉Vue3.0的正式版将要在本月(8月)发布,从发布到正式投入到正式项目中,还需要一定的过渡期,但我们不能一直等到Vue3正式投入到项目中的时候才去学习,提前学习,让你更快一步掌握Vue3.0,升职加薪迎娶白富美就靠它了。不过在学习Vue3之前,还需要先了解一下Proxy,它是Vue3.0实现数据双向绑定的基础。

本文是作者关于Vue3.0系列的第一篇文章,后续作者将会每周发布一篇Vue3.0相关,如果喜欢,麻烦给小编一个赞,谢谢

了解代理模式

一个例子

作为一个单身钢铁直男程序员,小王最近逐渐喜欢上了前台小妹,不过呢,他又和前台小妹不熟,所以决定委托与前端小妹比较熟的UI小姐姐帮忙给自己搭桥引线。小王于是请UI小姐姐吃了一顿大餐,然后拿出一封情书委托它转交给前台小妹,情书上写的 我喜欢你,我想和你睡觉,不愧钢铁直男。不过这样写肯定是没戏的,UI小姐姐吃人嘴短,于是帮忙改了情书,改成了我喜欢你,我想和你一起在晨辉的沐浴下起床,然后交给了前台小妹。虽然有没有撮合成功不清楚啊,不过这个故事告诉我们,小王活该单身狗。

其实上面就是一个比较典型的代理模式的例子,小王想给前台小妹送情书,因为不熟所以委托UI小姐姐UI小姐姐相当于代理人,代替小王完成了送情书的事情。

引申

通过上面的例子,我们想想Vue的数据响应原理,比如下面这段代码


const xiaowang = {
  love: '我喜欢你,我想和你睡觉'
}
// 送给小姐姐情书
function sendToMyLove(obj) {
    console.log(obj.love)
    return '流氓,滚'
}
console.log(sendToMyLove(xiaowang))

如果没有UI小姐姐代替送情书,显示结局是悲惨的,想想Vue2.0的双向绑定,通过Object.defineProperty来监听的属性 get,set方法来实现双向绑定,这个Object.defineProperty就相当于UI小姐姐

const xiaowang = {
  loveLetter: '我喜欢你,我想和你睡觉'
}
// UI小姐姐代理
Object.defineProperty(xiaowang,'love', {
  get() {
    return xiaowang.loveLetter.replace('睡觉','一起在晨辉的沐浴下起床')
  }
})

// 送给小姐姐情书
function sendToMyLove(obj) {
    console.log(obj.love)
    return '小伙子还挺有诗情画意的么,不过老娘不喜欢,滚'
}
console.log(sendToMyLove(xiaowang))

虽然依然是一个悲惨的故事,因为送奔驰的成功率可能会更高一些。但是我们可以看到,通过Object.defineproperty可以对对象的已有属性进行拦截,然后做一些额外的操作。

存在的问题

Vue2.0中,数据双向绑定就是通过Object.defineProperty去监听对象的每一个属性,然后在get,set方法中通过发布订阅者模式来实现的数据响应,但是存在一定的缺陷,比如只能监听已存在的属性,对于新增删除属性就无能为力了,同时无法监听数组的变化,所以在Vue3.0中将其换成了功能更强大的Proxy

了解Proxy

ProxyES6新推出的一个特性,可以用它去拦截js操作的方法,从而对这些方法进行代理操作。

用Proxy重写上面的例子

比如我们可以通过Proxy对上面的送情书情节进行重写:

const xiaowang = {
  loveLetter: '我喜欢你,我想和你睡觉'
}
const proxy = new Proxy(xiaowang, {
  get(target,key) {
    if(key === 'loveLetter') {
      return target[key].replace('睡觉','一起在晨辉的沐浴下起床')
    }
  }
})
// 送给小姐姐情书
function sendToMyLove(obj) {
    console.log(obj.loveLetter)
    return '小伙子还挺有诗情画意的么,不过老娘不喜欢,滚'
}
console.log(sendToMyLove(proxy))

再看这样一个场景

请分别使用Object.definePropertyProxy完善下面的代码逻辑.

function observe(obj, callback) {}

const obj = observe(
  {
    name: '子君',
    sex: '男'
  },
  (key, value) => {
    console.log(`属性[${key}]的值被修改为[${value}]`)
  }
)

// 这段代码执行后,输出 属性[name]的值被修改为[妹纸]
obj.name = '妹纸'

// 这段代码执行后,输出 属性[sex]的值被修改为[女]
obj.sex = '女'

看了上面的代码,希望大家可以先自行实现以下,下面我们分别用Object.definePropertyProxy去实现上面的逻辑.

  1. 使用Object.defineProperty
/**
 * 请实现这个函数,使下面的代码逻辑正常运行
 * @param {*} obj 对象
 * @param {*} callback 回调函数
 */
function observe(obj, callback) {
  const newObj = {}
  Object.keys(obj).forEach(key => {
    Object.defineProperty(newObj, key, {
      configurable: true,
      enumerable: true,
      get() {
        return obj[key]
      },
      // 当属性的值被修改时,会调用set,这时候就可以在set里面调用回调函数
      set(newVal) {
        obj[key] = newVal
        callback(key, newVal)
      }
    })
  })
  return newObj
}

const obj = observe(
  {
    name: '子君',
    sex: '男'
  },
  (key, value) => {
    console.log(`属性[${key}]的值被修改为[${value}]`)
  }
)

// 这段代码执行后,输出 属性[name]的值被修改为[妹纸]
obj.name = '妹纸'

// 这段代码执行后,输出 属性[sex]的值被修改为[女]
obj.name = '女'
  1. 使用Proxy
function observe(obj, callback) {
  return new Proxy(obj, {
    get(target, key) {
      return target[key]
    },
    set(target, key, value) {
      target[key] = value
      callback(key, value)
    }
  })
}

const obj = observe(
  {
    name: '子君',
    sex: '男'
  },
  (key, value) => {
    console.log(`属性[${key}]的值被修改为[${value}]`)
  }
)

// 这段代码执行后,输出 属性[name]的值被修改为[妹纸]
obj.name = '妹纸'

// 这段代码执行后,输出 属性[sex]的值被修改为[女]
obj.name = '女'

通过上面两种不同实现方式,我们可以大概的了解到Object.definePropertyProxy的用法,但是当给对象添加新的属性的时候,区别就出来了,比如

// 添加公众号字段
obj.gzh = '前端有的玩'

使用Object.defineProperty无法监听到新增属性,但是使用Proxy是可以监听到的。对比上面两段代码可以发现有以下几点不同

  • Object.defineProperty监听的是对象的每一个属性,而Proxy监听的是对象自身
  • 使用Object.defineProperty需要遍历对象的每一个属性,对于性能会有一定的影响
  • Proxy对新增的属性也能监听到,但Object.defineProperty无法监听到。

初识Proxy

概念与语法

MDN中,关于Proxy是这样介绍的: Proxy 对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)。什么意思呢?Proxy就像一个拦截器一样,它可以在读取对象的属性,修改对象的属性,获取对象属性列表,通过for in循环等等操作的时候,去拦截对象上面的默认行为,然后自己去自定义这些行为,比如上面例子中的set,我们通过拦截默认的set,然后在自定义的set里面添加了回调函数的调用

Proxy的语法格式如下

/**
* target: 要兼容的对象,可以是一个对象,数组,函数等等
* handler: 是一个对象,里面包含了可以监听这个对象的行为函数,比如上面例子里面的`get`与`set`
* 同时会返回一个新的对象proxy, 为了能够触发handler里面的函数,必须要使用返回值去进行其他操作,比如修改值
*/
const proxy = new Proxy(target, handler)

在上面的例子里面,我们已经使用到了handler里面提供的getset方法了,接下来我们一一看一下handler里面的方法。

handler 里面的方法列表

handler里面的方法可以有以下这十三个,每一个都对应的一种或多种针对proxy代理对象的操作行为

  1. handler.get

    当通过proxy去读取对象里面的属性的时候,会进入到get钩子函数里面

  2. handler.set

    当通过proxy去为对象设置修改属性的时候,会进入到set钩子函数里面

  3. handler.has

    当使用in判断属性是否在proxy代理对象里面时,会触发has,比如

    const obj = {
      name: '子君'
    }
    console.log('name' in obj)
  4. handler.deleteProperty

    当使用delete去删除对象里面的属性的时候,会进入deleteProperty`钩子函数

  5. handler.apply

    proxy监听的是一个函数的时候,当调用这个函数时,会进入apply钩子函数

  6. handle.ownKeys

    当通过Object.getOwnPropertyNames,Object.getownPropertySymbols,Object.keys,Reflect.ownKeys去获取对象的信息的时候,就会进入ownKeys这个钩子函数

  7. handler.construct

    当使用new操作符的时候,会进入construct这个钩子函数

  8. handler.defineProperty

    当使用Object.defineProperty去修改属性修饰符的时候,会进入这个钩子函数

  9. handler.getPrototypeOf

    当读取对象的原型的时候,会进入这个钩子函数

  10. handler.setPrototypeOf

    当设置对象的原型的时候,会进入这个钩子函数

  11. handler.isExtensible

    当通过Object.isExtensible去判断对象是否可以添加新的属性的时候,进入这个钩子函数

  12. handler.preventExtensions

    当通过Object.preventExtensions去设置对象不可以修改新属性时候,进入这个钩子函数

  13. handler.getOwnPropertyDescriptor

    在获取代理对象某个属性的属性描述时触发该操作,比如在执行 Object.getOwnPropertyDescriptor(proxy, "foo") 时会进入这个钩子函数

Proxy提供了十三种拦截对象操作的方法,本文主要挑选其中一部分在Vue3中比较重要的进行说明,其余的建议可以直接阅读MDN关于Proxy的介绍。

详细介绍

get

当通过proxy去读取对象里面的属性的时候,会进入到get钩子函数里面

当我们从一个proxy代理上面读取属性的时候,就会触发get钩子函数,get函数的结构如下

/**
 * target: 目标对象,即通过proxy代理的对象
 * key: 要访问的属性名称
 * receiver: receiver相当于是我们要读取的属性的this,一般情况
 *           下他就是proxy对象本身,关于receiver的作用,后文将具体讲解
 */
handle.get(target,key, receiver)
示例

我们在工作中经常会有封装axios的需求,在封装过程中,也需要对请求异常进行封装,比如不同的状态码返回的异常信息是不同的,如下是一部分状态码及其提示信息:

// 状态码提示信息
const errorMessage = {
  400: '错误请求',
  401: '系统未授权,请重新登录',
  403: '拒绝访问',
  404: '请求失败,未找到该资源'
}

// 使用方式
const code = 404
const message = errorMessage[code]
console.log(message)

但这存在一个问题,状态码很多,我们不可能每一个状态码都去枚举出来,所以对于一些异常状态码,我们希望可以进行统一提示,如提示为系统异常,请联系管理员,这时候就可以使用Proxy对错误信息进行代理处理

// 状态码提示信息
const errorMessage = {
  400: '错误请求',
  401: '系统未授权,请重新登录',
  403: '拒绝访问',
  404: '请求失败,未找到该资源'
}

const proxy = new Proxy(errorMessage, {
  get(target,key) {
    const value = target[key]
    return value || '系统异常,请联系管理员'
  }
})

// 输出 错误请求
console.log(proxy[400])
// 输出 系统异常,请联系管理员
console.log(proxy[500])

set

当为对象里面的属性赋值的时候,会触发set

当给对象里面的属性赋值的时候,会触发set,set函数的结构如下

/**
 * target: 目标对象,即通过proxy代理的对象
 * key: 要赋值的属性名称
 * value: 目标属性要赋的新值
 * receiver: 与 get的receiver 基本一致
 */
handle.set(target,key,value, receiver)
示例

某系统需要录入一系列数值用于数据统计,但是在录入数值的时候,可能录入的存在一部分异常值,对于这些异常值需要在录入的时候进行处理, 比如大于100的值,转换为100, 小于0的值,转换为0, 这时候就可以使用proxyset,在赋值的时候,对数据进行处理

const numbers = []
const proxy = new Proxy(numbers, {
  set(target,key,value) {
    if(value < 0) {
      value = 0
    }else if(value > 100) {
      value = 100
    }
    target[key] = value
    // 对于set 来说,如果操作成功必须返回true, 否则会被视为失败
    return true
  }
})

proxy.push(1)
proxy.push(101)
proxy.push(-10)
// 输出 [1, 100, 0]
console.log(numbers)
对比Vue2.0

在使用Vue2.0的时候,如果给对象添加新属性的时候,往往需要调用$set, 这是因为Object.defineProperty只能监听已存在的属性,而新增的属性无法监听,而通过$set相当于手动给对象新增了属性,然后再触发数据响应。但是对于Vue3.0来说,因为使用了Proxy, 在他的set钩子函数中是可以监听到新增属性的,所以就不再需要使用$set

const obj = {
  name: '子君'
}
const proxy = new Proxy(obj, {
  set(target,key,value) {
    if(!target.hasOwnProperty(key)) {
      console.log(`新增了属性${key},值为${value}`)
    }
    target[key] = value
    return true
  }
})
// 新增 公众号 属性
// 输出 新增了属性gzh,值为前端有的玩
proxy.gzh = '前端有的玩'

has

当使用in判断属性是否在proxy代理对象里面时,会触发has
/**
 * target: 目标对象,即通过proxy代理的对象
 * key: 要判断的key是否在target中
 */
 handle.has(target,key)
示例

一般情况下我们在js中声明私有属性的时候,会将属性的名字以_开头,对于这些私有属性,是不需要外部调用,所以如果可以隐藏掉是最好的,这时候就可以通过has在判断某个属性是否在对象时,如果以_开头,则返回false

const obj =  {
  publicMethod() {},
  _privateMethod(){}
}
const proxy = new Proxy(obj, {
  has(target, key) {
    if(key.startsWith('_')) {
      return false
    }
    return Reflect.get(target,key)
  }
})

// 输出 false
console.log('_privateMethod' in proxy)

// 输出 true
console.log('publicMethod' in proxy)

deleteProperty

当使用delete去删除对象里面的属性的时候,会进入deleteProperty`拦截器
/**
 * target: 目标对象,即通过proxy代理的对象
 * key: 要删除的属性
 */
 handle.deleteProperty(target,key)
示例

现在有一个用户信息的对象,对于某些用户信息,只允许查看,但不能删除或者修改,对此使用Proxy可以对不能删除或者修改的属性进行拦截并抛出异常,如下

const userInfo = {
  name: '子君',
  gzh: '前端有的玩',
  sex: '男',
  age: 22
}
// 只能删除用户名和公众号
const readonlyKeys = ['name', 'gzh']
const proxy = new Proxy(userInfo, {
  set(target,key,value) {
    if(readonlyKeys.includes(key)) {
      throw new Error(`属性${key}不能被修改`)
    }
    target[key] = value
    return true
  },
   deleteProperty(target,key) {
    if(readonlyKeys.includes(key)) {
      throw new Error(`属性${key}不能被删除`)
      return
    }
    delete target[key]
    return true
  }
})
// 报错 
delete proxy.name
对比Vue2.0

其实与$set解决的问题类似,Vue2.0是无法监听到属性被删除的,所以提供了$delete用于删除属性,但是对于Proxy,是可以监听删除操作的,所以就不需要再使用$delete

其他操作

在上文中,我们提到了Proxyhandler提供了十三个函数,在上面我们列举了最常用的三个,其实每一个的用法都是基本一致的,比如ownKeys,当通过Object.getOwnPropertyNames,Object.getownPropertySymbols,Object.keys,Reflect.ownKeys去获取对象的信息的时候,就会进入ownKeys这个钩子函数,使用这个我们就可以对一些我们不像暴露的属性进行保护,比如一般会约定_开头的为私有属性,所以在使用Object.keys去获取对象的所有key的时候,就可以把所有_开头的属性屏蔽掉。关于剩余的那些属性,建议大家多去看看MDN中的介绍。

Reflect

在上面,我们获取属性的值或者修改属性的值都是通过直接操作target来实现的,但实际上ES6已经为我们提供了在Proxy内部调用对象的默认行为的API,即Reflect。比如下面的代码

const obj = {}
const proxy = new Proxy(obj, {
  get(target,key,receiver) {
    return Reflect.get(target,key,receiver)
  }
})

大家可能看到上面的代码与直接使用target[key]的方式没什么区别,但实际上Reflect的出现是为了让Object上面的操作更加规范,比如我们要判断某一个prop是否在一个对象中,通常会使用到in,即

const obj = {name: '子君'}
console.log('name' in obj)

但上面的操作是一种命令式的语法,通过Reflect可以将其转变为函数式的语法,显得更加规范

Reflect.has(obj,'name')

除了has,get之外,其实Reflect上面总共提供了十三个静态方法,这十三个静态方法与Proxyhandler上面的十三个方法是一一对应的,通过将ProxyReflect相结合,就可以对对象上面的默认操作进行拦截处理,当然这也就属于函数元编程的范畴了。

总结

有的同学可能会有疑惑,我不会ProxyReflect就学不了Vue3.0了吗?其实懂不懂这个是不影响学习Vue3.0的,但是如果想深入 去理解Vue3.0,还是很有必要了解这些的。比如经常会有人在使用Vue2的时候问,为什么我数组通过索引修改值之后,界面没有变呢?当你了解到Object.defineProperty的使用方式与限制之后,就会恍然大悟,原来如此。本文之后,小编将为大家带来Vue3.0系列文章,欢迎关注,一起学习。同时本文首发于公众号【前端有的玩】,用玩的姿势学前端,就在【前端有的玩】

查看原文

赞 36 收藏 22 评论 0

Margin 收藏了文章 · 8月18日

移动端适配资源整理

Margin 收藏了文章 · 8月18日

移动端页面布局及字体大小该如何设置

之前发过一篇文章《移动端应该如何动态设置字体大小?》,主要说了移动web端布局的一些解决方法,本文再一次把这个问题提出来,并分别对安卓和IOS设备的屏幕了解做出自己的分享,在进入正文之前最好先了解:物理像素逻辑像素DPRRem

那么进入正文,不废话,直接把自己了解到的和一些看法说出来。

首先是屏幕问题,现在主流的移动设备以安卓和IOS为主,我们在制作移动端页面也是以兼容这两种设备去布局。

首先说iPhone,不得不说iPhone的屏幕考虑到了我们开发者的难处,从而给出iPhone屏幕的dpr都是整数值,在6plus出现之前,iphone的dpr始终是2(物理像素/逻辑像素=2),即使是6plus出现了,iphone到底其实也就只有2,3这两个dpr。其实6plus的实际dpr并不是整数,而是2.87左右,不过,为了方便开发者来开发,iphone6plus对其做了一个调整,将dpr调整为3,然后在对屏幕进行了一个缩放。所以我们很容易对其做到兼顾。

而安卓的dpr值,并不像iphone那样就只有两个值。安卓的dpr是千奇百怪的,可能是1.5,2,3,4,2.5等等的都有。(甚至我还看到了1.7之类的,安卓的各个设备商,玩的真尼玛high啊。怎么高兴怎么来。)

那么现在开始说说移动端怎么布局以及字体该怎么设置,因为有各种各样的解决方式,我就不一一赘述,直接说手淘的解决方案:flexible.js

我为什么又一次把这个拿出来说,主要有两点原因:1.我觉得它好用,解决方式简单粗暴。2.它经过了比较长时间的考验,如今手淘还在用它。

具体的使用方法自己可以去flexible.js看看,这里我简单说说它的方案以及个人对它的改良。

我们UI在制作移动端页面时,主流的宽度有640、750或者还有其他尺寸,这里我们用640做为例子,那么手淘的做法就是将640的宽度分为10份,每份是64,那么1个rem=64px,也就是说此时 html{font-size:64px;},比如设计稿中有个元素宽64px,高128px,那么这个div的宽用rem表示就是:64(设计稿元素的宽)/64(1rem为64px)=该元素计算出的rem值,同理高度从px换成rem就是128/64=2rem样式就是div{width:1rem;height:2rem;}。然后再配上flexible.js的代码段:

function refreshRem(){
        var width = docEl.getBoundingClientRect().width;
        if (width / dpr > 540) {
            width = 540 * dpr;
        }
        var rem = width / 10;//看到了吗 看到了吗 这句话 分成10份哦
        docEl.style.fontSize = rem + 'px';
        flexible.rem = win.rem = rem;
    }

对于布局,我们可以使用rem去结局,那么字体呢?

大家知道,现在PC端主流的字体大小都是类似12px、14px、16px,几乎没有13、15、17这样的数字出现,至于为什么,请进穿越门了解。如果我们用rem做为字体单位,那么转成px的时候,势必会出现奇数或者小数的情况,为了避免这种情况,我们还是要用px做为字体的单位。那么又如何用px去当作字体大小呢?

首先看这段代码:

div {
    width: 1rem; 
    height: 0.4rem;
    font-size: 12px; // 默认写上dpr为1的fontSize
}

[data-dpr="2"] div {
    font-size: 24px;
}

[data-dpr="3"] div {
    font-size: 36px;
}

没有错,手淘的解决方案就是默认写个dpr为1时的字体大小,然后根据不同dpr下的值去匹配不同的字体大小。

clipboard.png

flexible会获取设备的dpr值,然后在html标签上自定义data-dpr属性,并放入dpr值,后面的font-size就是动态设置一个rem单位的大小。

其实我觉得这种方案挺恶心的,每个牵扯到字体的大小就必须多些几套去兼容,但这也算是个方案。

最后就是对flexible.js的看法,话不多说上代码:

if (!dpr && !scale) {
        var isAndroid = win.navigator.appVersion.match(/android/gi);
        var isIPhone = win.navigator.appVersion.match(/iphone/gi);
        var devicePixelRatio = win.devicePixelRatio;
        if (isIPhone) {//只对iPhone做了处理!!!!
            // iOS下,对于2和3的屏,用2倍的方案,其余的用1倍方案
            if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {                
                dpr = 3;
            } else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)){
                dpr = 2;
            } else {
                dpr = 1;
            }
        } else {
            // 其他设备下,仍旧使用1倍的方案
            dpr = 1;
        }
        scale = 1 / dpr;
    }

可以看出,flexible中动态设置dpr时,只对iPhone进行了处理,完全没有把安卓放在眼里,那安卓怎么办?这里我又想吐槽安卓那些厂商,真尼玛瞎搞,dpr设置成各种非主流数值,玩死我们这些码农了,连flexible都不想管了。吐槽到此为止,那么该怎么改良它,让它能对安卓手机也能动态设置dpr呢?我们将这段代码改一下:

if (!dpr && !scale) {
    //devicePixelRatio这个属性是可以获取到设备的dpr
    var devicePixelRatio = win.devicePixelRatio;
    //判断dpr是否为整数
    var isRegularDpr = devicePixelRatio.toString().match(/^[1-9]\d*$/g)
    if (isRegularDpr) {
    // 对于是整数的dpr,对dpr进行操作
     if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {                
        dpr = 3;
    } else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)){
        dpr = 2;
    } else {
        dpr = 1;
    }
} else {
    // 对于其他的dpr,人采用dpr为1的方案
    dpr = 1;
    }
    scale = 1 / dpr;
}

我们对这里做了一点点修改,即来判断dpr是否是规则的,也就是是否是我们常见的1,2,3等,然后,我们只对规则的dpr,来进行一个字体的处理。这样,iphone依然还是用之前的匹配方案。而其实目前安卓,很多的设备还是比较常见的dpr了,所以我们这里,将之前对设备的判断,转变成对dpr是否是整数的一个判断。其他地方不变,可以解决对安卓dpr的部分匹配。

查看原文

Margin 赞了文章 · 8月18日

移动端页面布局及字体大小该如何设置

之前发过一篇文章《移动端应该如何动态设置字体大小?》,主要说了移动web端布局的一些解决方法,本文再一次把这个问题提出来,并分别对安卓和IOS设备的屏幕了解做出自己的分享,在进入正文之前最好先了解:物理像素逻辑像素DPRRem

那么进入正文,不废话,直接把自己了解到的和一些看法说出来。

首先是屏幕问题,现在主流的移动设备以安卓和IOS为主,我们在制作移动端页面也是以兼容这两种设备去布局。

首先说iPhone,不得不说iPhone的屏幕考虑到了我们开发者的难处,从而给出iPhone屏幕的dpr都是整数值,在6plus出现之前,iphone的dpr始终是2(物理像素/逻辑像素=2),即使是6plus出现了,iphone到底其实也就只有2,3这两个dpr。其实6plus的实际dpr并不是整数,而是2.87左右,不过,为了方便开发者来开发,iphone6plus对其做了一个调整,将dpr调整为3,然后在对屏幕进行了一个缩放。所以我们很容易对其做到兼顾。

而安卓的dpr值,并不像iphone那样就只有两个值。安卓的dpr是千奇百怪的,可能是1.5,2,3,4,2.5等等的都有。(甚至我还看到了1.7之类的,安卓的各个设备商,玩的真尼玛high啊。怎么高兴怎么来。)

那么现在开始说说移动端怎么布局以及字体该怎么设置,因为有各种各样的解决方式,我就不一一赘述,直接说手淘的解决方案:flexible.js

我为什么又一次把这个拿出来说,主要有两点原因:1.我觉得它好用,解决方式简单粗暴。2.它经过了比较长时间的考验,如今手淘还在用它。

具体的使用方法自己可以去flexible.js看看,这里我简单说说它的方案以及个人对它的改良。

我们UI在制作移动端页面时,主流的宽度有640、750或者还有其他尺寸,这里我们用640做为例子,那么手淘的做法就是将640的宽度分为10份,每份是64,那么1个rem=64px,也就是说此时 html{font-size:64px;},比如设计稿中有个元素宽64px,高128px,那么这个div的宽用rem表示就是:64(设计稿元素的宽)/64(1rem为64px)=该元素计算出的rem值,同理高度从px换成rem就是128/64=2rem样式就是div{width:1rem;height:2rem;}。然后再配上flexible.js的代码段:

function refreshRem(){
        var width = docEl.getBoundingClientRect().width;
        if (width / dpr > 540) {
            width = 540 * dpr;
        }
        var rem = width / 10;//看到了吗 看到了吗 这句话 分成10份哦
        docEl.style.fontSize = rem + 'px';
        flexible.rem = win.rem = rem;
    }

对于布局,我们可以使用rem去结局,那么字体呢?

大家知道,现在PC端主流的字体大小都是类似12px、14px、16px,几乎没有13、15、17这样的数字出现,至于为什么,请进穿越门了解。如果我们用rem做为字体单位,那么转成px的时候,势必会出现奇数或者小数的情况,为了避免这种情况,我们还是要用px做为字体的单位。那么又如何用px去当作字体大小呢?

首先看这段代码:

div {
    width: 1rem; 
    height: 0.4rem;
    font-size: 12px; // 默认写上dpr为1的fontSize
}

[data-dpr="2"] div {
    font-size: 24px;
}

[data-dpr="3"] div {
    font-size: 36px;
}

没有错,手淘的解决方案就是默认写个dpr为1时的字体大小,然后根据不同dpr下的值去匹配不同的字体大小。

clipboard.png

flexible会获取设备的dpr值,然后在html标签上自定义data-dpr属性,并放入dpr值,后面的font-size就是动态设置一个rem单位的大小。

其实我觉得这种方案挺恶心的,每个牵扯到字体的大小就必须多些几套去兼容,但这也算是个方案。

最后就是对flexible.js的看法,话不多说上代码:

if (!dpr && !scale) {
        var isAndroid = win.navigator.appVersion.match(/android/gi);
        var isIPhone = win.navigator.appVersion.match(/iphone/gi);
        var devicePixelRatio = win.devicePixelRatio;
        if (isIPhone) {//只对iPhone做了处理!!!!
            // iOS下,对于2和3的屏,用2倍的方案,其余的用1倍方案
            if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {                
                dpr = 3;
            } else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)){
                dpr = 2;
            } else {
                dpr = 1;
            }
        } else {
            // 其他设备下,仍旧使用1倍的方案
            dpr = 1;
        }
        scale = 1 / dpr;
    }

可以看出,flexible中动态设置dpr时,只对iPhone进行了处理,完全没有把安卓放在眼里,那安卓怎么办?这里我又想吐槽安卓那些厂商,真尼玛瞎搞,dpr设置成各种非主流数值,玩死我们这些码农了,连flexible都不想管了。吐槽到此为止,那么该怎么改良它,让它能对安卓手机也能动态设置dpr呢?我们将这段代码改一下:

if (!dpr && !scale) {
    //devicePixelRatio这个属性是可以获取到设备的dpr
    var devicePixelRatio = win.devicePixelRatio;
    //判断dpr是否为整数
    var isRegularDpr = devicePixelRatio.toString().match(/^[1-9]\d*$/g)
    if (isRegularDpr) {
    // 对于是整数的dpr,对dpr进行操作
     if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {                
        dpr = 3;
    } else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)){
        dpr = 2;
    } else {
        dpr = 1;
    }
} else {
    // 对于其他的dpr,人采用dpr为1的方案
    dpr = 1;
    }
    scale = 1 / dpr;
}

我们对这里做了一点点修改,即来判断dpr是否是规则的,也就是是否是我们常见的1,2,3等,然后,我们只对规则的dpr,来进行一个字体的处理。这样,iphone依然还是用之前的匹配方案。而其实目前安卓,很多的设备还是比较常见的dpr了,所以我们这里,将之前对设备的判断,转变成对dpr是否是整数的一个判断。其他地方不变,可以解决对安卓dpr的部分匹配。

查看原文

赞 16 收藏 121 评论 7

Margin 收藏了文章 · 7月31日

不要再问我移动适配的问题了

“不要再问我XX的问题”系列:
一、不要再问我this的指向问题了
二、不要再问我跨域的问题了

移动端适配的问题,一般来说我们都不会去深究,因为这种东西都是配置一次就再也不用管的了,接到设计图就按照祖传套路撸就完事了。按部就班的必定只能成为活动页写手,研究透彻以后,才能成为一名专业的活动页写手嘛。

纠缠不清的关系

文章开始,我们需要来捋清楚像素、视口以及缩放之间种种藕断丝连的关系,来抽丝剥茧一波。

像素

像素我们写得多了,不就是px嘛,为什么要拿出来说呢?因为像素还不仅仅就是px。

  • 设备像素

设备像素也可以叫物理像素,由设备的屏幕决定,其实就是屏幕中控制显示的最小单位。

  • 设备独立像素

设备独立像素是一种可以被程序所控制的虚拟像素,在Web开发中对应CSS像素。

  • DPR

设备像素与设备独立像素之间的关系就是,DPR(设备像素比),设备像素比 = 设备像素 / 设备独立像素。这条公式成立的前提是,缩放比为1,原因下面讲到缩放的时候就会知道。根据这种关系,如果设备像素大于设备独立像素(DPR大于1的设备,我们常说的高清屏或者Retina屏),就会出现一个设备独立像素对应多个设备像素的情况:
图片描述

视口

遥想从前智能手机刚出的时候,很少网站去特意适配移动端,然而用户是可以直接从手机去访问PC端网站的,所以怎样显示好一个网站,无论这个网站是一个PC网站还是移动端网站,就是亟需解决的问题。所以移动端三个视口布局视口、视觉视口、理想视口横空出世,成为各种移动适配方案的基础。

  • 布局视口

布局视口是在html元素之上的容器,我们的页面就“装”在布局视口中。
想想我们常写的width:100%,这个100%是基于什么计算出来的呢?去翻资料会看到:如果某些属性被赋予一个百分值的话,它的计算值是由这个元素的包含块计算而来的。那html元素的包含块是什么呢?没错,就是我们的布局视口,它是所有CSS百分比推算的根源,如果说CSS是一支画笔,那么布局视口就是那张画布吧。这张画布有一个默认尺寸(如果没有手动去设置meta viewport),一般在768px ~ 1024px间,可以通过document.documentElement.clientWidth获取。这样一来,网页的布局就不再受限于设备的尺寸,即使是小屏幕的移动端设备中也能容得下PC网站。
布局视口

  • 视觉视口

视觉视口是指用户通过设备屏幕看到的区域,可以通过缩放来改变视觉视口的大小,并通过window.innerWidth获取。
这里有必要讲一下缩放,缩放改变的是CSS像素的大小,放大时CSS像素增大,则一个CSS像素可以跨越更多的设备像素,视觉视口会变小。什么?放大反而视觉视口变小?没错,这是因为视觉视口也是通过CSS像素度量,而放大就是使CSS像素放大,假设屏幕上本来需要200个CSS像素才能占满屏幕,由于放大,现在只需要100个CSS像素就能占满,所以视觉视口的宽就变成100px。
虽然缩放改变了CSS像素的大小,但移动端的缩放是不会改变布局视口的,所以缩放并不会影响布局,不过在PC端是会影响布局的。最直观的感受是,我们平时在移动端双指缩放网页,整个网页的布局是没有变化的,可以通过拖动来看到不同区域的东西,但是在PC端进行缩放,比如阅读时想文字大一些而对网页进行放大操作,这时字是放大了,但整个页面的布局会有所改变。那么既然与布局视口无关那还跟谁有关系呢?答案就是下面准备要讲的理想视口,它们之间的计算方式是:缩放系数 = 理想视口宽度 / 视觉视口宽度
视觉视口

  • 理想视口

理想视口是指网站在移动设备中的理想大小,这个大小就是设备的屏幕大小。
为什么需要理想视口呢?首先,先来看看现在的情况是怎么的不理想。我们在浏览一个没经过移动适配的网站时,由于布局视口在768px ~ 1024px之间,整个网站就“画”在一个这么大的“画布”上,但由于手机屏幕比“画布”小,所以需要经过缩小才能塞进手机屏幕,结果我们浏览网站的时候虽然看得见全貌,但里面的东西都变得很小,需要放大一下才能看得清,就是这么不理想。如果不需要放大就可以看得清那就很理想了嘛。回想一下上面不理想的解决方案,就是将一个大画布经过缩小装进小屏幕里,假设现在画布跟屏幕一样大,就在这个画布上作画,岂不是很合适?
所以总结起来,理想视口说白了就是理想的布局视口,通过<meta name="viewport" content="width=device-width, initial-scale=1">来设置。

将它们连在一起

关联

认识Meta viewport

<meta> 元素可提供有关页面的元信息,不会显示在页面上,可以用来告诉浏览器怎样解析页面。<meta>可以设置的东西很多,但这里只讲vieport,它是所有移动适配方案的基础。
首先meta viewport的设置格式是<meta name="viewport" content="name=value,name=value",其中name的值可设为:

  1. width:将布局视口设置为固定的值,比如375px或者device-width(设备宽度)
  2. initial-scale:设置页面的初始缩放
  3. minimum-scale:设置最小的缩小程度
  4. maximum-scale:设置最大的放大程度
  5. user-scalable:设置为no时禁用缩放

虽然只有五个值,但仍有一些值得注意的点:

设置initial-scale的影响

根据公式缩放系数 = 理想视口宽度 / 视觉视口宽度 ,如果设置了initial-scale比如为0.5,那么以iPhone6为例,iPhone6的设备宽度是375px,即理想视口宽度也为375px,所以视觉视口宽度 = 375px(理想视口宽度)/ 0.5(缩放系数)。很明显设置了initial-scale就相当于初始化了视觉视口,而且会将布局视口初始化为这个视觉视口的值

width和initial-scale共存

上面说到设置了initial-scale相当于初始化了视觉视口和布局视口,但width用于指定布局视口的大小,那么一起设置的话听谁的呢?
还是以iPhone6为例,它的尺寸是667(h) * 375(w),如果设置<meta name="viewport" content="width=400, initial-scale=1">,执行一下console.log(`布局视口: ${document.documentElement.clientWidth}; 视觉视口: ${window.innerWidth}`)会得到“布局视口: 400; 视觉视口: 400”。
这时候旋转一下设备,这时尺寸变成了667(w) * 375(h),再执行一下console.log(`布局视口: ${document.documentElement.clientWidth}; 视觉视口: ${window.innerWidth}`)会得到“布局视口: 667; 视觉视口: 667”。
结论是:width与initial-scale都会初始化布局视口,但浏览器会取其最大值。

设置理想视口

这时候再看回<meta name="viewport" content="width=device-width, initial-scale=1">,明明width=device-width和initial-scale=1都是去初始化布局视口成理想的布局视口,只写其中一个不就完了嘛,为什么要两个都一起写呢?因为有的浏览器只设置其中一个,不能保证理想视口的尺寸能随着屏幕的旋转而正确改变,所以两个一起写只是为了解决兼容性问题。

舒服地还原移动端设计图

上面说了很多理论知识,其实就是为了能有一套方案舒服地还原移动端设计图,做出一个专为移动端访问的页面。

经典的问题

  • 图片

这里的图片问题是指高清/Retina屏下图片会显示得比较模糊,这是因为我们平时使用的图片大多数是png、jpg这样格式的图片,它们称作是位图图像(bitmap),是由一个个像素点构成,缩放会失真。上面讲像素的时候说过,这种高清/Retina屏DPR大于一,则一像素横跨了多个设备像素,而位图图像需要一个像素点对应一个设备像素才清晰。所以假设一张100 x 100的图片放在普通屏上看是清晰的,放到高清/Retina屏上就会显得比较模糊,那是因为本来100 x 100的图片在普通屏上图片像素与设备像素一一对应,而到了高清/Retina屏上一个图片像素却要对应多个设备像素,这样一来看起来图片就比较模糊。
图片问题
如图所示,如果一个图片像素要对应多个设备像素的话,那这些设备像素只能显示成跟这个图片像素差不多的颜色,导致看起来会模糊。
既然知道了问题产生的原因,那解决方法也很简单,位图图像需要一个像素点对应一个设备像素才清晰嘛,那就本来是100 x 100的图片在DPR为1的屏幕上显示清晰,在DPR为2的屏幕上显示模糊,那就在DPR为2的屏幕上放200 x 200的图好了,这样就一一对应了。

  • 1px边框

1px边框
“你看看设计图这根线是很细的,为什么你实现出来那么粗,看起来很劣质的感觉。”
没道理呀,设计图量的是1px,css写的也是1px,怎么会粗了呢?一般设计师出图的时候,都会按照一个尺寸作为标准来出图,比如按照iPhone6的尺寸出图,就是一张750px宽的设计图,这个750px其实就是iPhone6的设备像素,在测量设计图时量到的1px其实是1设备像素,而当我们设置<meta name="viewport" content="width=device-width, initial-scale=1">时,布局视口等于理想视口等于375px,并且由于iPhone6的DPR为2,写css时的1px对应的是2设备像素,所以看起来会粗一点。
那么只要写0.5px就是对应1设备像素了嘛。是的,道理是这么说,但是很多浏览器并不支持0.5px的写法,导致显示不出来,但不要紧,网上很多方法解决这个问题的方法就不细说了,这里只是讲清楚1px边框问题产生的原因。

还原设计图

因为PC端屏幕一般都会比设计图尺寸要大,所以只需要居中固定一个内容区用于显示设计图的内容,其余多出的地方留白即可。而移动端屏幕有大有小,设计图一般会以一款机型为标准来出图,比如说iPhone6的尺寸,如果不经处理直接量设计图就开干会出现什么问题呢?
对比
(从左到右为iPhone4、iPhone6、iPhone plus)
可以看到以iPhone6为标准出的设计图测量出来350px x 350px的元素在iPhone6上写width: 350px;height: 350px;是刚刚好的,左右的间隙各有10px,但小一点的屏幕iPhone4横向滚动条都出来了,而plus左右间隙明显比10px大很多,这样一来不同尺寸的屏幕出来的效果跟设计图的效果就会有不同程度的出入,这并不是我们想要的,我们想要的是不同尺寸的屏幕显示的效果与设计图比例是一致的。
既然想要的是不同屏幕尺寸显示的比例与设计图一致,那么显然适配方案就是等比缩放
(以下代码都是为了讲述原理,没有过多的细节考虑与测试,不能用于生产环境)

  • viewport方案

说到缩放,首先想到的当然是initial-scale。回想一下initial-scale的作用:设置了initial-scale就相当于初始化了视觉视口,而且会将布局视口初始化为这个视觉视口的值。那么我们是不是可以以设计图为基准等比缩放布局视口从而适配呢?

<script>
    const scale = window.screen.width / 750
    document.write(`<meta name="viewport" content="initial-scale=${scale}">`)
</script>

这种方式进行适配优点是简单粗暴,缺点是太简单粗暴了,因为viewport的设置是影响全局的,这样一来虽然可以直接将设计图量得的尺寸写到css上,但如果有一些需要地方不需要等比缩放而需要设置固定尺寸,比如要求在不同尺寸屏幕上显示固定大小的文字,或者你引进了一个库,里面的有样式你也不知道人家是按照怎样的适配方案进行适配的,那么到了你的项目里由于全局的viewport缩放,可能会影响到这个库的显示效果。

  • rem方案

不同于px是固定尺寸单位,rem是相对单位,相对于html标签字体大小的单位。比如html标签的font-size为100px,那么1rem就等于100px。借助rem这个相对单位我们同样可以达到等比缩放的效果。

  • 这个方案不需要对viewport进行缩放,所以首先按照惯例我们让布局视口等于理想视口:<meta name="viewport" content="width=device-width, initial-scale=1">
  • 还是以iPhone6的设备像素为标准的设计图,宽是750px,假设以设计图为标准的html标签的font-size为100px,所以1rem = 100px,那么这个设计图总宽就有7.5rem
  • 以总宽是7.5rem的设计图为标准,则不同屏幕尺寸的总宽应该也是7.5rem,由于上面设置了布局视口等于理想视口,所以以iPhone6为例,iPhone6的布局视口等于理想视口,则它的布局视口为375px(也就是总宽7.5rem),现在只需要解决在布局视口为375px的情况下,html的font-size需要设置多少。很简单,html font-size * 7.5 = 375,那么font-size为50px。
  • 拓展到其他屏幕document.documentElement.style.fontSize = `${document.documentElement.clientWidth / 7.5}px`
  • 现在我们只需要测量设计图,比如设计图有一个300px的元素,那我们写css的时候就写成3rem(由于以1rem = 100px为基准,所以这里300px / 100即可)

使用这个方案,我们只对需要等比缩放的元素使用rem,而要求固定尺寸的地方使用px即可,这样一来相对于viewport方案来说就比较灵活,可以按需使用而不是一刀切。不过这种方案写css的时候可能会没那么直观,成本可能会高一点点,但是借助构建工具或者less/sass可以解决,毕竟现在应该很少项目不使用这些工具的了吧。

  • 加强版rem方案

这里所说的加强版rem方案其实就是手淘的Flexible方案(也类似移动端高清、多屏适配方案),究竟加强了什么呢?那就是,通过设置viewport进而全局解决1px边框问题。
既然要通过设置viewport来解决1px边框问题,那设置这个viewport的方式肯定内有乾坤:

if (!dpr && !scale) {
    var isAndroid = win.navigator.appVersion.match(/android/gi);
    var isIPhone = win.navigator.appVersion.match(/iphone/gi);
    var devicePixelRatio = win.devicePixelRatio;
    if (isIPhone) {
        if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {                
            dpr = 3;
        } else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)){
            dpr = 2;
        } else {
            dpr = 1;
        }
    } else {
        // 其他设备下,仍旧使用1倍的方案
        dpr = 1;
    }
    scale = 1 / dpr;
}

得出的scale用于设置viewport的缩放document.write(`<meta name="viewport" content="initial-scale=${scale}">`),这样一来,对于Retina屏将viewport缩放为1 / dpr最终产生的效果是,1px css像素严格等于1px 设备像素,由此解决了1px边框问题。那为什么只对iPhone进行缩放呢?请看大漠老师的文章再谈Retina下1px的解决方案
其他与rem相关的配置与上面的rem方案类似,这里就不再展开说了。
这个加强版rem方案最大的优势是解决了1px边框问题,但由此也进行了viewport的缩放,仍然会面临着上面说的viewport方案涉及到的一些影响,为此该方案会通过给html设置data-dpr

document.documentElement.setAttribute('data-dpr', dpr)

从而写css的时候可以针对不同的dpr固定设置尺寸:

.test {
    width: 1rem; 
    height: 2rem;
    font-size: 12px; 
}
[data-dpr="2"] .test {
    font-size: 13px;
}
[data-dpr="3"] .test {
    font-size: 14px;
}
  • vw方案

vw也是一个相对单位,它相对的是布局视口,1vw就是1%的布局视口宽度。其实rem方案就是在模拟vw,来看看使用vw怎么做。

  1. 还是熟悉的iPhone6标准设计图,宽750px。那么1vw = 1%视口宽度的话,按设计图来说就是100vw = 750px,则1vw = 7.5px。
  2. 设计图量得一个元素是100px,css需要写成 Xvw * 7.5 = 100,所以X就等于13.3vw。
  3. 计算的话还是交给构建工具即可,详细请看再聊移动端页面的适配

rem方案有的优势vw也有,而且也不会像rem那么绕,但就是兼容性不够rem好,长远来看vw最后会接棒rem作为移动适配的主力,因为它生来就干这个事情呢。

终于结束了

没有银弹。
全局viewport缩放方案很粗暴?但对于要求不高也不需要兼顾固定尺寸的页面,上来就全局缩放,拿起设计稿就可以写代码了。要求高又想灵活,还会怕构建的那一点点麻烦吗?rem方案走起。兼容性不需要考虑,那vw方案直白又优雅不试试看吗?方案没有优劣之分只有合适与否。
最后,如果有说得不对的地方,还望指正。

查看原文

Margin 赞了回答 · 7月22日

无法复制input内文字

有没有设置这两个属性oncopy="return false;" oncut="return false;
还有csss是不是设置了不可复制属性

关注 1 回答 1

认证与成就

  • 获得 3 次点赞
  • 获得 8 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 8 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2018-06-15
个人主页被 194 人浏览