cookie定义

用户访问网络,服务器给当前客户机上创建的一个临时文件用来保存用户信息,以便用户下次继续访问该网络时,网站服务器能识别用户身份,常见cookie用来保存用户界面,用户id等资源。

cookie的使用

1.创建cookie:当前页面设置cookie不是立即生效,等下一个页面才能看到。

1
2
3
4
5
6
setcookie('name','mary',time()+24*60*60) //一天
cookie名称 cookie值 有效时间
应用:
if($_POST['name']&&$_POST['password']){
setcookie('name','mary',time()+24*60*60);
}

设置有效时间,cookie会保存在硬盘里,不设置,则保存在内存里,用户关掉页面就消失了。
cookie是http协议头的一部分,用于浏览器和服务器之间传递信息,cookie是通过http报头发送的。

2.接受cookie

1
2
3
if(isset($_COOKIE('name'))){
echo $_COOKIE('name');
}

3.删除cookie

1
setcookie('name','',time()-1);

如何利用实现自动登录:
当用户在某个网站注册后,就会收到一个惟一用户ID的cookie。客户后来重新连接时,这个用户ID会自动返回,服务器对它进行检查,确定它是否为注册用户且选择了自动登录,从而使用户务需给出明确的用户名和密码,就可以访问服务器上的资源。

session

session定义

服务器机制,使用类似散列表的结构来保存信息,php会给每一个访问用户创建一个session id,该id唯一,它的存放有三种方式:
· url传递:安全性差
· 保存在客户端的cookie中:php会话机制通过设置cookie,在cookie中保存会话id(session id),在服务器会生成session文件,与用户关联。
· 保存在数据库中:安全,但是效率下降。

session的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
启动session:
session_start() //必须放在文件最前面

创建session:
$_SESSION['name'] = "hello";
$_SESSION['arr'] = array("name"=>"hello","type"=>"web");
echo $_SESSION['name'];

删除session:
unset($_SESSION['name']);

清空用户所有的session信息:
session_destroy();

当程序为某个用户创建一个session,服务器首先检查客户端请求是够包含session标识(session id),若有,则服务器按照session id把session检索出来使用;若无,则服务器创建一个session并且生成一个与此相关联的session id,这个session id将在本次响应中返回给客户端保存。

cookie与session之间的联系

具体来说cookie机制采用的是在客户端保持状态的方案,而session机制采用的是在服务器端保持状态的方案。同时我们也看到,由于才服务器端保持状态的方案在客户端也需要保存一个标识,所以session机制可能需要借助于cookie机制来达到保存标识的目的,但实际上还有其他选择.

RSA算法被认为是目前地球上最重要的加密算法,所以作为互联网的热爱者来说,想了解一下它的神秘感,加深对网络安全方面的认识。

在数论那一章铺垫了很多要实现RSA算法的准备工作,现在主要讲解一下如何生成密钥。

密钥生成步骤

假设A要和B进行加密通信,她就需要生成公钥和私钥。公钥用来给A加密信息,私钥用来解密。

1.第一步,随机选择两个不相等的质数p和q
假设A选择了61和53。(实际应用中,这两个质数越大,就越难破解。)

2.第二步,计算p和q的乘积n

1
n = 61×53 = 3233

n的长度就是密钥长度。3233写成二进制是110010100001,一共有12位,所以这个密钥就是12位。实际应用中,RSA密钥一般是1024位,重要场合则为2048位。

3.第三步,计算n的欧拉函数φ(n)

1
φ(n) = (p-1)(q-1)

算出φ(3233)等于60×52,即3120。

4.第四步,随机选择一个整数e,条件是1< e < φ(n),且e与φ(n) 互质
在1到3120之间,随机选择了17。(实际应用中,常常选择65537。)

5.第五步,计算e对于φ(n)的模反元素d
所谓”模反元素”就是指有一个整数d,可以使得ed被φ(n)除的余数为1。

1
2
3
4
5
6
7
    ed ≡ 1 (mod φ(n))
即:
ed + 1 = φ(n)x
已知 e=17, φ(n)=3120,
所以:
17d +1 =3120x
求出一组解为(d,x)=(2753,-15) ,即d = 2753

至此所有计算完成。

6.第六步,将n和e封装成公钥,n和d封装成私钥。
n=3233,e=17,d=2753,所以公钥就是 (3233,17),私钥就是(3233, 2753)
实际应用中,公钥和私钥的数据都采用ASN.1格式表达(实例)。

算法可靠性

密钥生成步骤中,(n,e)是公钥,是公开的,p,q,φ(n),d是不公开的,其中最关键的是d,因为n和d组成了私钥,一旦d泄漏,就等于私钥泄漏。

那么,有无可能在已知n和e的情况下,推导出d?

1
2
3
(1)ed≡1 (mod φ(n))。只有知道e和φ(n),才能算出d。
(2)φ(n)=(p-1)(q-1)。只有知道p和q,才能算出φ(n)。
(3)n=pq。只有将n因数分解,才能算出p和q。

结论:如果n可以被因数分解,d就可以算出,也就意味着私钥被破解。
可是,大整数的因数分解,是一件非常困难的事情。目前,除了暴力破解,还没有发现别的有效方法。

加密和解密

(1)加密要用公钥 (n,e)
假设B要向A发送加密信息m,他就要用A的公钥 (n,e) 对m进行加密。这里需要注意,m必须是整数(字符串可以取ascii值或unicode值),且m必须小于n。
所谓”加密”,就是算出下式的c:

1
 c = ( m^e) mod n

A的公钥是 (3233, 17),B的m假设是65,那么可以算出下面的等式:

1
  c =( 65^17) mod 3233 =  2790

c = 2790,B把2790发给A

(2)解密要用私钥(n,d)
A收到B发过来的2790后,就用自己的私钥(3233,2753)进行解密。

1
2
3
m = (c^d) mod n
即:
m = (2790^2753) mod 3233 = 65

A知道了B加密前的原文就是65。

至此,”加密–解密”的整个过程全部完成。
我们可以看到,如果不知道d,就没有办法从c求出m。而前面已经说过,要知道d就必须分解n,这是极难做到的,所以RSA算法保证了通信安全。
你可能会问,公钥(n,e) 只能加密小于n的整数m,那么如果要加密大于n的整数,该怎么办?有两种解决方法:一种是把长信息分割成若干段短消息,每段分别加密;另一种是先选择一种”对称性加密算法”(比如DES),用这种算法的密钥加密信息,再用RSA公钥加密DES密钥。

参考文章:
http://www.ruanyifeng.com/blog/2013/06/rsa_algorithm_part_one.html

一些概念

1.对称密钥

1
2
(1)甲方选择某一种加密规则,对信息进行加密;
(2)乙方使用同一种规则,对信息进行解密。

这种加密模式有一个最大弱点:甲方必须把加密规则告诉乙方,否则无法解密。保存和传递密钥,就成了最头疼的问题。

2.非对称密钥

1
2
3
(1)乙方生成两把密钥(公钥和私钥)。公钥是公开的,任何人都可以获得,私钥则是保密的。
(2)甲方获取乙方的公钥,然后用它对信息加密。
(3)乙方得到加密后的信息,用私钥解密。

如果公钥加密的信息只有私钥解得开,那么只要私钥不泄漏,通信就是安全的。

3.互质关系

如果两个正整数,除了1以外,没有其他公因子,我们就称这两个数是互质关系

4.欧拉函数

思考:任意给定正整数n,请问在小于等于n的正整数之中,有多少个与n构成互质关系?(比如,在1到8之中,有多少个数与8构成互质关系?)

计算这个值的方法就叫做欧拉函数,以φ(n)表示。在1到8之中,与8形成互质关系的是1、3、5、7,所以 φ(n) = 4。
φ(n) 的计算方法并不复杂,但是为了得到最后那个公式,需要一步步讨论。

1) 情况一
如果n=1,则 φ(1) = 1 。因为1与任何数(包括自身)都构成互质关系。

2) 情况二
如果n是质数,则 φ(n)=n-1 。因为质数与小于它的每一个数,都构成互质关系。比如5与1、2、3、4都构成互质关系。

3) 情况三
如果n是质数的某一个次方,即 n = p^k (p为质数,k为大于等于1的整数),则
φ(p^k) = p^k - p^(k-1) = p^k(1-1/p)
比如 φ(8) = φ(2^3) =2^3 - 2^2 = 8 -4 = 4。
可以看出,上面的第二种情况是 k=1 时的特例。

4) 情况四
如果n可以分解成两个互质的整数之积,n = p1 * p2,则
φ(n) = φ(p1p2) = φ(p1)φ(p2)
即积的欧拉函数等于各个因子的欧拉函数之积。比如,φ(56)=φ(8×7)=φ(8)×φ(7)=4×6=24。

5) 情况五
因为任意一个大于1的正整数,都可以写成一系列质数的积。

根据第四条结论,得到:

在根据第三条结论,得到:

也就等于:

这就是欧拉函数的通用计算公式。比如,1323的欧拉函数,计算过程如下:

5.欧拉定理

欧拉函数的用处,在于欧拉定理欧拉定理指的是:
如果两个正整数a和n互质,则n的欧拉函数 φ(n) 可以让下面的等式成立:

也就是说,a的φ(n)次方被n除的余数为1。或者说,a的φ(n)次方减去1,可以被n整除。比如,3和7互质,而7的欧拉函数φ(7)等于6,所以3的6次方(729)减去1,可以被7整除(728/7=104)。

欧拉定理可以大大简化某些运算。比如,7和10互质,根据欧拉定理,

已知 φ(10) 等于4,所以马上得到7的4倍数次方的个位数肯定是1。

因此,7的任意次方的个位数(例如7的222次方),心算就可以算出来。7^φ(223)%10 = 1

欧拉定理有一个特殊情况:设正整数a与质数p互质,因为质数p的φ(p)等于p-1,则欧拉定理可以写成

这就是著名的费马小定理。它是欧拉定理的特例
欧拉定理是RSA算法的核心。理解了这个定理,就可以理解RSA。

6.模反元素

如果两个正整数a和n互质,那么一定可以找到整数b,使得 ab-1 被n整除,或者说ab被n除的余数是1。

这时,b就叫做a的模反元素

比如,3和11互质,那么3的模反元素就是4,因为 (3 × 4)-1 可以被11整除。显然,模反元素不止一个, 4加减11的整数倍都是3的模反元素 {…,-18,-7,4,15,26,…},即如果b是a的模反元素,则 b+kn 都是a的模反元素。

计算模反元素方法:
x*n+1可以除尽a则为模反元素

欧拉定理可以用来证明模反元素必然存在。

可以看到,a的 φ(n)-1 次方,就是a的模反元素。

参考文章:
http://www.ruanyifeng.com/blog/2013/06/rsa_algorithm_part_one.html

前言

最近在赶安卓项目,做了一个日记app,原生安卓开发,刚入门安卓,各方面还不是很懂,但一个项目下来自己能学到很多,servlet,多线程,网络访问等等,其中也遇到不少问题,拿来与大家分享。

问题与解决方案

  1. 新建低版本的项目时马上出现appcompat_v7上有个红色的叉提示错误时,右键点击两个项目,(appcompat_v7和test)都要打开properties,然后取消下图3的打勾,打上4的勾
    图1

  2. 在下图红色框中切换代码和调试模式

  3. 如果想要使用Xutils框架的东西的话,需要进行初始化:把这个类作为安卓程序的接入点:

    并且需要在AndroidManifest.xml文件里面加上

    1
    2
    3
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.INTERNET" />(这句表示允许网络连接)
    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
  4. 如果从其他项目复制activity到另外一个项目,只要把另一个项目里的AndroidManifest.xml里面相关模块加上就能用了,例如:

  5. 在服务器中建立文件夹的方式:
    String folderPath =”images/“+user.getUserName();
    String targetFile = request.getSession().getServletContext().getRealPath(“/“ + folderPath);
    File dir = new File(targetFile);

  6. 安卓的TextView设置字符一定要为String类型,不然会出错,整型要转化String类型,例如:
    String text_concern.setText(Integer.toString(concernN));

  7. 服务器出现如下问题:

    出现这个错误是因为在一个错误的包新建了servlet,然后又把这个servlet拖到其他的包里,这样就会导致web.xml里面的配置描述不匹配,然后服务器实例化servlet的时候就会导致错误。解决的办法就是把下面的那个包名描述改回来。(web.xml)

  8. 关注列表点击闪退到登录界面,原因是忘记了传currentUser参数到日记正文,导致缺少参数出错,解决办法是在上一个页面跳转中加上需要传的参数,如:intent.putExtra(“currentuser”, currentuser);

  9. 使用ImageLoader 外部类,必须先初始化,不然会报错。初始化方法:
    ImageLoader.getInstance().init(ImageLoaderConfiguration.createDefault(Activity.this));

  10. MyEclipse启动本地Tomcat出错:Cannot change deployment state from ERROR to REDEPLOYING.
    解决办法:
    右击“项目名” –>myeclipse–>add and remove project deployments,然后在Server选项卡里删除出错的项目,重新部署即可,但是此时tomcat7里面的数据会丢失,所以最好先保存一份数据。

  11. 返回键的简单应用:Activity.this.finish();

暂时写这么多吧,其实一些细节问题挺多的,但作为安卓入门来说却是一大收获。

面试前端的时候有时会提到网络方面的问题,趁热打铁,温习一下关于http相关知识。

认识http

http(超文本传输协议)是一个基于请求与响应模式的、无状态的应用层的协议,常基于TCP的连接方式。
http的特点:

  1. 使用面向连接的TCP作为运输层协,保证数据可靠性。
  2. 无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
  3. 无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。

http协议通常承载与TCP协议之上,有时也承载于TLS或者SSL协议层之上,这个时候,就变成常见的HTTPS。默认http端口号是80,https的端口号是443.
http协议

http版本

http1.0

http1.0是无连接方式,所以浏览器每次发送http请求,就会建立一条TCP连接,等待服务器端响应后,这条连接就会断开。等下次再有请求的时候,需要再建立一条TCP连接,这样导致每次有2倍RTT往返时间(TCP占一个RTT,HTTP请求与响应占一个RTT),增加了网络开销。

好在浏览器都提供了嫩能够打开5-10个并行的TCP连接,而每一个连接处理客户的一个请求。因此,使用并行TCP连接可以缩短响应时间。

http1.1

http1.1改进了http1.0的缺点,http1.1采用持续连接,即只要建立一个TCP连接,就可以在这条连接上进行复用。优点是可以减少重复进行TCP三次握手的开销,提高效率。
http1.1分为两类:

  1. 非流水线方式:在同一个TCP连接中,新的请求需要等上次请求的响应后,才能发送。因此,在TCP连接建立以后,客户每访问一次对象就要用去一个往返时间RTT,这比非持续连接的两倍RTT的开销节省了建立TCP连接所需的一个RTT时间。
  2. 流水线方式:在同一个TCP连接中,客户在收到http响应报文之前就能够接着发送新的请求报文。于是一个接一个的请求报文到达服务器后,服务器就可以连续发送响应报文。使用流水线方式时,客户访问所有的对象只需花费一个RTT的时间,使TCP连接中的空闲时间减少,提高下载文档的速度。

http2.0

http2.0提出服务器推的概念,服务器可以对一个客户端的请求发送多个响应,除了最初的请求外,还会额外想客户端推送资源,而无需客户端明确请求,客户端还可以缓存资源。

http2.0的请求都在一条TCP连接上,允许多个信息在一条连接上同时交叉,采用并行双向字节流的请求和响应,只使用一个连接,可以并行交错发送请求和响应。

增加了首部压缩,即使非常小的请求,其请求与响应的header都只会占用很小比例的带宽。

http请求与响应报文格式

http请求报文和响应报文都是由开始行首部行实体主体,而请求与响应报文的不同在于开始行不一样。

请求格式

请求行 = 方法 + 请求资源的URL + http版本 + [回车换行CRLF]
例子如下:

1
2
3
4
5
6
7
8
9
GET /hello.htm HTTP/1.1
Accept: */*
Accept-Language: zh-cn
Accept-Encoding: gzip, deflate
If-Modified-Since: Wed, 17 Oct 2016 02:15:55 GMT
If-None-Match: W/"158-1192587355000"
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)
Host: 192.168.2.162:8080
Connection: Keep-Alive

响应格式

状态行 = http版本 + 状态码 + 解释状态码的简单短语 + [回车换行CRLF]
例子如下:

1
2
3
4
5
6
7
HTTP/1.1 200 OK
ETag: W/"158-1192590101000"
Last-Modified: Wed, 17 Oct 2016 03:01:41 GMT
Content-Type: text/html
Content-Length: 158
Date: Wed, 17 Oct 2016 03:01:59 GMT
Server: Apache-Coyote/1.1

总结

  http网络这块对于理解前端优化,服务器端优化来说还是很有必要的,理解后很多东西就豁然开朗了,所以说基础知识还是要打牢,并且随着互联网的发展,新出的技术可以使以前一些繁琐的处理变得简单甚至变得没有必要。

这几天看了一些关于网络方面的知识,总结一下域名系统DNS(Domain Name System)的查找过程。

基本概念

定义

定义很容易理解:DNS就是用来把便于人们使用的机器名字转换成ip地址。

域名结构

1
2
3
     mail.qq.com
/ | \
三级域名.二级域名.顶级域名

域名服务器

  1. 根域名服务器:最高层次的域名服务器,也是最重要的域名服务器。所有的根域名服务器都知道所有的顶级域名服务器的域名和IP地址。不管是哪一个本地域名服务器,若要对因特网上任何一个域名进行解析,只要自己无法解析,就首先求助根域名服务器。在很多情况下,根域名服务器并不直接把待查询的域名直接解析出IP地址,而是告诉本地域名服务器下一步应当找哪一个顶级域名服务器进行查询。
  2. 顶级域名服务器:负责管理在该顶级域名服务器注册的二级域名。
  3. 权限域名服务器:负责一个“区”的域名服务器。
  4. 本地域名服务器:当一个主机发出DNS查询请求时,这个查询请求报文就发送给本地域名服务器。

DNS查询方式分类

  1. 递归查询:主机所询问的本地域名服务器不知道被查询的域名ip地址,那么本地服务器就以DNS客户的身份,向其他的根域名服务器继续发出查询请求报文,不论成功与否,都会做出对应的响应。(发生在DNS客户端与DNS服务器之间)
    递归查询
  2. 迭代查询:DNS服务器根据自己的高速缓存或区域的数据,以最佳结果响应。如果DNS服务器无法解析,它可能返回一个指针。指针指向下级域名的DNS服务器,继续该过程。知道找到拥有所查询名字的DNS服务器,或者出错,超时为止。(发生在DNS服务器之间)
    迭代查询

DNS查询过程

假设主机想查询另一个主机的www.163.com的IP地址,则查询步骤如下:

  1. 主机向本地DNS服务器进行递归查询,浏览器检查缓存中有没有这个域名对应的解析过的ip地址,如果缓存中有,则查询过程结束。
  2. 否则本地服务器采用迭代查询。它先向一个根域名服务器查询,全球只有13台根域名服务器。
  3. 根域名服务器告诉本地服务器,下一次应查询的顶级域名服务器dns.com的IP地址。
  4. 本地域名服务器向顶级域名服务器dns.com进行查询。
  5. 顶级域名服务器dns.com告诉本地域名服务器,下一步应查询的权限服务器dns.163.com的IP地址。
  6. 本地域名服务器向权限域名服务器dns.163.com进行查询。
  7. 权限域名服务器dns.163.com告诉本地域名服务器,所查询的主机的IP地址。
  8. 本地域名服务器最后把查询结果告诉主机。
    引用别人的一张图片:
    DNS查询过程

整个查询过程共用到了8个UDP报文。
为了提高DNS查询效率,并减轻服务器的负荷和减少因特网上的DNS查询报文数量,在域名服务器中广泛使用了高速缓存,用来存放最近查询过的域名以及从何处获得域名映射信息的记录。

CommonJS规范

上两篇博客中提到的AMD``CMD都是浏览器端的模块开发方案,而CommonJS是服务器端模块的规范,Node.js采用了这个规范。Node.JS首先采用了js模块化的概念。

根据CommonJS规范,一个单独的文件就是一个模块。每一个模块都是一个单独的作用域,也就是说,在该模块内部定义的变量,无法被其他模块读取,除非定义为global对象的属性。

输出模块变量的最好方法是使用module.exports对象。

加载模块使用require方法,该方法读取一个文件并执行,最后返回文件内部的module.exports对象(之前所用的exports其实是module.exports的一个引用,详情)。

1
2
3
4
5
6
7
8
9
var i = 0;
var max = 30;

module.exports = function () {
for (i; i++ < max; ) {
console.log(i);
}
max * = 1.1;
};

上面代码通过module.exports对象,定义了一个函数,该函数就是模块外部与内部通信的桥梁。
加载模块使用require方法,该方法读取一个文件并执行,最后返回文件内部的module.exports对象。

为什么CommonJS适用于服务器端呢?需要分析一下浏览器端的js和服务器端js都主要做了哪些事,有什么不同了:

1
2
3
4
5
6
7
--------------------------------服务器端JS   |    浏览器端JS-------------------------------

相同的代码需要多次执行 | 代码需要从一个服务器端分发到多个客户端执行

CPU和内存资源是瓶颈 | 带宽是瓶颈

加载时从磁盘中加载 | 加载时需要通过网络加载

CommonJS加载模块是同步的,所以只有加载完成才能执行后面的操作。像Node.js主要用于服务器的编程,加载的模块文件一般都已经存在本地硬盘,所以加载起来比较快,不用考虑异步加载的方式,所以CommonJS规范比较适用。但如果是浏览器环境,要从服务器加载模块,这时就必须采用异步模式。所以就有了 AMD CMD 解决方案。

不过,Node.js还没有看咧,待我学好了再来深入了解这个规范吧~~

CMD

CMD规范

CMD规范文档
CMD(Common Module Definition) 通用模块定义。该规范明确了模块的基本书写格式和基本交互规则。该规范是在国内发展出来的。AMD是依赖关系前置,CMD是按需加载。
CMDSeaJS 在推广过程中对模块定义的规范化产出。
在 CMD 规范中,一个模块就是一个文件。代码的书写格式如下:

1
2
3
define(function(require, exports, module) {
// 模块代码
});

define方法在执行时,默认会传入三个参数:requireexportsmodule。如果需要一个文件中加载多个define()函数,则需要定义模块名。
require是可以把其他模块导入进来的一个参数,而export是可以把模块内的一些属性和方法导出的。

seajs模块引入与自定义(手打RequireJS相关代码地址

require.js类似,seajs使用base作为根目录:

1
2
3
4
5
6
7
8
9
10
<script type="text/javascript" src="js/sea.js"></script>
<script type="text/javascript">
seajs.config({
base:"./js",
alias:{
"range":"module/range.js"
}
})
seajs.use("app.js");
</script>

主模块加载

require.js不同,seajs加载模块的时候,require函数是写在define函数里面的,下面的例子,其实可以不用写上模块名和依赖模块(模块名要把 ID 定为文件路理由)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
define("app",["module/drag","module/scale"],function (require,exports,module){
var oInput = document.getElementById('input1');
var oDiv1 = document.getElementById('div1');
var oDiv2 = document.getElementById('div2');
var oDiv3 = document.getElementById('div3');

require("module/drag.js").drag(oDiv3);
oInput.onclick = function(){
oDiv1.style.display = "block";
require("module/scale.js").scale(oDiv1,oDiv2);
//require 是同步往下执行,require.async 则是异步回调执行。require.async 一般用来加载可延迟异步加载的模块。
require.async("module/scale.js",function(ex){
ex.scale(oDiv1,oDiv2);
})
}
})

注意,有时你想使用 require 来进行条件加载:

1
2
3
4
5
6
7
define(function(require){
if(false) {
require("module-A");
} else {
require("module-B");
}
});

但是,require无法完成条件加载,从静态分析的角度来看,这个模块同时依赖 module-A 和 module-B 两个模块,加载器会把这两个模块文件都下载下来。 这种情况下,推荐使用 require.async 来进行条件加载。

其实区别就是一点,require模块的加载过程是发生在模块源码分析期; require.async加载发生在模块执行期。

AMD和CMD

AMD 是 RequireJS 在推广过程中对模块定义的规范化产出。
CMD 是 SeaJS 在推广过程中对模块定义的规范化产出。

对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。

AMD:提前执行(异步加载:依赖先执行)+延迟执行
CMD:延迟执行(运行到需加载,根据顺序执行)

CMD 推崇依赖就近,AMD 推崇依赖前置。看如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// CMD
define(function(require, exports, module) {
var a = require('./a')
a.doSomething()

var b = require('./b') // 依赖可以就近书写
b.doSomething()

})

// AMD 默认推荐的是
define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好
a.doSomething()

b.doSomething()

})

另外一个区别是:

AMD:API根据使用范围有区别,但使用同一个api接口
CMD:每个API的职责单一

AMD的优点是:异步并行加载,在AMD的规范下,同时异步加载是不会产生错误的。
CMD的机制则不同,这种加载方式会产生错误,如果能规范化模块内容形式,也可以

RequireJSSea.js 都是模块加载器,倡导模块化开发理念,核心价值是让 JavaScript 的模块化开发变得简单自然。

SeaJSRequireJS最大的区别:

SeaJS对模块的态度是懒执行, 而RequireJS对模块的态度是预执行。

RequireJS的做法是并行加载所有依赖的模块, 并完成解析后, 再开始执行其他代码, 因此执行结果只会”停顿”1次, 完成整个过程是会比SeaJS要快.

而SeaJS一样是并行加载所有依赖的模块, 但不会立即执行模块, 等到真正需要(require)的时候才开始解析, 这里耗费了时间, 所以说的SeaJS是”懒执行”.

先知先觉

随着前端项目越来越复杂,使用模块化开发可以提高代码的重用性,并且使项目结构清晰化。一个模块就是实现特定功能的文件,有了模块,我们就可以更方便地使用别人的代码,想要什么功能,就加载什么模块。模块开发需要遵循一定的规范,否则就都乱套了。

目前,通行的js模块规范主要由三种:AMD,CMD,CommonJS.

简单来说,就是使用define定义模块,使用require调用模块。

先盗用一张图来方便理解他们的异同:
module

AMD规范

AMD规范文档
AMDAsynchronous Module Definition,中文名是异步模块定义的意思。它是一个在浏览器端模块化开发的规范,服务器端的规范是CommonJS

模块将被异步加载,模块加载不影响后面语句的运行。所有依赖某些模块的语句均放置在回调函数中。

AMDRequireJS 在推广过程中对模块定义的规范化的产出。

优点:

  1. 实现js文件的异步加载,避免网页失去响应;
  2. 管理模块之间的依赖性,便于代码的编写和维护。

目前,实现AMD的库有RequireJScurlDojoNodules 等。

define() 函数

AMD规范只定义了一个函数 define,它是全局变量。函数的描述为:

1
define(id?, dependencies?, factory);

id:指定义中模块的名字,可选;如果没有提供该参数,模块的名字应该默认为模块加载器请求的指定脚本的名字。如果提供了该参数,模块名必须是“顶级”的和绝对的(不允许相对名字)。

依赖dependencies:是一个当前模块依赖的,已被模块定义的模块标识的数组字面量。
依赖参数是可选的,如果忽略此参数,它应该默认为[“require”, “exports”, “module”]。然而,如果工厂方法的长度属性小于3,加载器会选择以函数的长度属性指定的参数个数调用工厂方法。

工厂方法factory,模块初始化要执行的函数或对象。如果为函数,它应该只被执行一次。如果是对象,此对象应该为模块的输出值。
(来自-segment)

模块名用来唯一标识定义中模块,它们同样在依赖性数组中使用,当一个文件中有多个define定义时,需要使用文件路径作为模块名来区分调用的模块是哪个。

模块名可以为 “相对的” 或 “顶级的”。如果首字符为“.”或“..”则为相对的模块名;如果你定义了根目录(baseUrl),那么顶级的模块名从根命名空间的概念模块解析.

函数定义例子:创建一个名为”alpha”的模块,使用了require,exports,和名为”beta”的模块:

1
2
3
4
5
6
7
define("alpha", ["require", "exports", "beta"], function (require, exports, beta) {
exports.verb = function() {
return beta.verb();
//Or:
return require("beta").verb();
}
});

RequireJS默认假定所有的依赖资源都是js脚本,因此无需在module ID上再加”.js”后缀,RequireJS在进行module ID到path的解析时会自动补上后缀。

require() 函数

可以看下面的实际例子:

1
2
3
require(['jquery','underscore','backbone'],function($, _, Backbone){
console.log($(".box").width());
});

require()函数接受两个参数。第一个参数是一个数组,表示所依赖的模块,上例就是['jquery', 'underscore', 'backbone'],即主模块依赖这三个模块;第二个参数是一个回调函数,当前面指定的模块都加载成功后,它将被调用。加载的模块会以参数形式传入该函数,从而在回调函数内部就可以使用这些模块。require()异步加载jqueryunderscorebackbone,浏览器不会失去响应;它指定的回调函数,只有前面的模块都加载成功后,才会运行,解决了依赖性的问题。

RequireJS模块的加载(手打RequireJS相关代码地址

调用require.js

1
<script src="js/require.js" data-main="js/main"></script>

data-main属性的作用是,指定网页程序的主模块。在上例中,就是js目录下面的main.js,这个文件会第一个被require.js加载。由于require.js默认的文件后缀名是js,所以可以把main.js简写成main。

模块加载自定义

使用require.config()方法,我们可以对模块的加载行为进行自定义。require.config()就写在主模块(main.js)的头部。参数就是一个对象,这个对象的paths属性指定各个模块的加载路径。

如果这些模块在其他目录,比如js/lib目录,则有两种写法。一种是逐一指定路径。

1
2
3
4
5
6
7
require.config({
    paths: {
      "jquery": "lib/jquery.min",
      "underscore": "lib/underscore.min",
      "backbone": "lib/backbone.min"
    }
  });

另一种则是直接改变基目录(baseUrl)。

1
2
3
4
5
6
7
8
require.config({
    baseUrl: "js/lib",
    paths: {
      "jquery": "jquery.min",
      "underscore": "underscore.min",
      "backbone": "backbone.min"
    }
  });

注意一点,如果加载require.js模块的时候出现timeout现象,解决办法:只要在config里面加上

1
waitSeconds: 0, //解决timeout问题

除了paths,还有shim用来加载非规范的模块,可以查看相关信息.

AMD模块的写法

require.js加载的模块,采用AMD规范。也就是说,模块必须按照AMD的规定来写。
具体来说,就是模块必须采用特定的define()函数来定义。如果一个模块不依赖其他模块,那么可以直接定义在define()函数之中。
假定现在有一个math.js文件,它定义了一个math模块。那么,math.js就要这样写:

1
2
3
4
5
6
7
8
9
// math.js
  define(function (){
    var add = function (x,y){
      return x+y;
    };
    return {
      add: add
    };
  });

加载方法如下:

1
2
3
4
// main.js
  require(['math'], function (math){
    alert(math.add(1,1));
  });

如果这个模块被其他模块依赖,那么define()函数的第一个参数,必须是一个数组,指明该模块的依赖性。

1
2
3
4
//myLib.js
define(['../module/math'],function(math){
return math.add(1,1);
})

当require()函数加载上面这个模块的时候,就会先加载myLib.js文件。

先写到这吧,有点累了,下次接着写。

参考文章:
1.https://github.com/amdjs/amdjs-api/wiki/AMD-(%E4%B8%AD%E6%96%87%E7%89%88)

前言

  对于移动端重构,又是一大令人头疼的问题,每次开始一个项目前,我都要认真思考一番使用何种布局方式会比较好,使用什么库比较好,使用的库会不会产生不兼容等种种问题。为了以后的我做项目前可以少一点纠结,我决定总结一下关于移动端如何进行适配的问题。
  相关代码示例已经上传到Github。   

背景

  1. 开发移动端H5页面
  2. 面对不同分辨率的手机
  3. 面对不同屏幕尺寸的手机

一些概念

  • 三个需要了解的概念:

    • PPI: 可以理解为屏幕的显示密度
    • DPR: 设备物理像素和逻辑像素的对应关系,即物理像素/逻辑像素
    • Resolution: 就是我们常说的分辨率
  • 物理像素与逻辑像素
    mobile
    看了我们上面内容一的第一点之后,或许有些人会有些疑问,我的安卓手机,或者iphone6plus(目前应该仅限于这一款机型吧),买回来的是1920x1080的或者其他更高的,比我之前所谓的那个viewport默认的980px要大。

    这样的问题,也就是我之前所说的物理像素与逻辑像素的关系了(即DPR)。以1920x1080为例,1080为物理像素,而我们在viewport中,获取到的,比如”width-device”,是逻辑像素。所以之前viewport的默认值,所比对的大小,其实是逻辑像素的大小,而非物理像素的大小。

    以iphone6为例,在不做任何缩放的条件下,iphone6的获取到的’width-device’为375px,为屏幕的逻辑像素。而购买时我们所知的750px,则为屏幕的物理像素。

  • 分辨率和像素
    经新xcode6模拟器验证(分辨率为pt,像素为真实pixel):

  1. iPhone5分辨率320×568,像素640×1136,@2x
  2. iPhone6分辨率375×667,像素750×1334,@2x
  3. iPhone6 Plus分辨率414×736,像素1242×2208,@3x,(注意,在这个分辨率下渲染后,图像等比降低pixel分辨率至1080p(1080×1920))
Resolution

  • 设备像素比DPR(device pixel ratio)
    在javascript中,可以通过window.devicePixelRatio获取到当前设备的dpr。

    在css中,可以通过-webkit-device-pixel-ratio-webkit-min-device-pixel-ratio-webkit-max-device-pixel-ratio进行媒体查询,对不同dpr的设备,做一些样式适配(这里只针对webkit内核的浏览器和webview)。

  • CSS的问题
    有了上面第二点的一些基础,还是以iphone6为例,我们可以知道,其实我们所写的1px,在iphone6上为2px的物理像素。所以,最后的,给出一个结论。就是我们写的1px,在移动端,是逻辑像素的1px,而非物理像素的1px。

    所以说,在iPhone6下,如果设计稿为750×1334,而设备宽高为375×667,由于它的DPR为2,所以说切下来的图不会失真;但是如果把这张图放到非retina 屏幕(即普通屏幕DPR为1)下,
    图片就相当于被压缩了,就出现一个物理像素点对应4个位图像素点,所以它的取色也只能通过一定的算法(显示结果就是一张只有原图像素总数四分之一,我们称这个过程叫做downsampling),肉眼看上去虽然图片不会模糊,但是会觉得图片缺少一些锐利度,或者是有点色差(但还是可以接受的)。

    反过来,如果在普通屏幕下切的图设计稿为375×667,放到retina手机上显示,图片就相当于拉伸了,即一个位图像素点要显示4个物理像素点,图片就会模糊。

    所以说,在retina屏幕下,设计稿的位图像素点个数可以跟物理像素点个数形成 1 : 1的比例,图片自然就清晰了(这也解释了为啥dpr为2的视觉稿的画布大小要×2)。

几个问题

retina下,图片高清问题

解决方案:两倍图片(@2x),然后图片容器缩小50%。

如:图片大小,400×600;

1.img标签

1
2
width: 200px;
height: 300px;

2.背景图片

1
2
3
4
width: 200px;
height: 300px;
background-image: url(image@2x.jpg);
background-size: 200px 300px; // 或者: background-size: contain;

这样的缺点,很明显,普通屏幕下:

  1. 同样下载了@2x的图片,造成资源浪费。
  2. 图片由于downsampling,会失去了一些锐利度(或是色差)。
    所以最好的解决办法是:不同的dpr下,加载不同的尺寸的图片。

不管是通过css媒体查询,还是通过javascript条件判断都是可以的。

retina下,border: 1px问题

设计师想要的retina下border: 1px;,其实就是1物理像素宽,对于css而言,可以认为是border: 0.5px;,这是retina下(dpr=2)下能显示的最小单位。
然而,无奈并不是所有手机浏览器都能识别border: 0.5px;,ios7以下,android等其他系统里,0.5px会被当成为0px处理,那么如何实现这0.5px呢?

最简单的一个做法就是这样(元素scale):

1
2
3
4
5
6
7
8
9
10
11
12
13
.scale{
position: relative;
}
.scale:after{
content:"";
position: absolute;
bottom:0px;
left:0px;
right:0px;
border-bottom:1px solid #ddd;
-webkit-transform:scaleY(.5);
-webkit-transform-origin:0 0;
}

我们照常写border-bottom: 1px solid #ddd;,然后通过transform: scaleY(.5)缩小0.5倍来达到0.5px的效果,但是这样hack实在是不够通用(如:圆角等),写起来也麻烦。

多屏适配布局问题

移动端布局,为了适配各种大屏手机,目前最好用的方案莫过于使用相对单位rem

基于rem的原理,我们要做的就是: 针对不同手机屏幕尺寸dpr动态的改变根节点html的font-size大小(基准值)。

这里我们提取了一个公式(rem表示基准值)

1
rem = document.documentElement.clientWidth * dpr / 10

说明:

  1. 乘以dpr,是因为页面有可能为了实现1px border页面会缩放(scale) 1/dpr 倍(如果没有,dpr=1),。
  2. 除以10,是为了取整,方便计算(理论上可以是任何值)
    所以就像下面这样,html的font-size可能会:
1
2
3
4
5
iphone3gs: 320px / 10 = 32px

iphone4/5: 320px * 2 / 10 = 64px

iphone6: 375px * 2 / 10 = 75px

对于动态改变根节点html的font-size,我们可以通过css做,也可以通过javascript做。
css方式,可以通过设备宽度来媒体查询来改变html的font-size:

1
2
3
4
5
6
7
8
9
html{font-size:32;}
//iphone6
@media (min-device-width:375){
html{font-size:75px;}
}
//iphone6 plus
@media (min-device-width:414px){
html{font-size:82.8px;}
}

缺点:通过设备宽度范围区间这样的媒体查询来动态改变rem基准值,其实不够精确,比如:宽度为360px 和 宽度为320px的手机,因为屏宽在同一范围区间内(<375px),所以会被同等对待(rem基准值相同),而事实上他们的屏幕宽度并不相等,它们的布局也应该有所不同。最终,结论就是:这样的做法,没有做到足够的精确,但是够用。

javascript方式,通过上面的公式,计算出基准值rem,然后写入样式,大概如下(代码参考自flexible.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
;(function(win, lib) {
var doc = win.document;
var docEl = doc.documentElement;
var metaEl = doc.querySelector('meta[name="viewport"]');
var flexibleEl = doc.querySelector('meta[name="flexible"]');
var dpr = 0;
var scale = 0;
var tid;
var flexible = lib.flexible || (lib.flexible = {});

if (metaEl) {
console.warn('将根据已有的meta标签来设置缩放比例');
var match = metaEl.getAttribute('content').match(/initial\-scale=([\d\.]+)/);
if (match) {
scale = parseFloat(match[1]);
dpr = parseInt(1 / scale);
}
} else if (flexibleEl) {
var content = flexibleEl.getAttribute('content');
if (content) {
var initialDpr = content.match(/initial\-dpr=([\d\.]+)/);
var maximumDpr = content.match(/maximum\-dpr=([\d\.]+)/);
if (initialDpr) {
dpr = parseFloat(initialDpr[1]);
scale = parseFloat((1 / dpr).toFixed(2));
}
if (maximumDpr) {
dpr = parseFloat(maximumDpr[1]);
scale = parseFloat((1 / dpr).toFixed(2));
}
}
}

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) {
// 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;
}

docEl.setAttribute('data-dpr', dpr);
if (!metaEl) {
metaEl = doc.createElement('meta');
metaEl.setAttribute('name', 'viewport');
metaEl.setAttribute('content', 'initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no');
if (docEl.firstElementChild) {
docEl.firstElementChild.appendChild(metaEl);
} else {
var wrap = doc.createElement('div');
wrap.appendChild(metaEl);
doc.write(wrap.innerHTML);
}
}

function refreshRem(){
var width = docEl.getBoundingClientRect().width;
if (width / dpr > 540) {
width = 540 * dpr;
}
var rem = width / 10;
docEl.style.fontSize = rem + 'px';
flexible.rem = win.rem = rem;
}

win.addEventListener('resize', function() {
clearTimeout(tid);
tid = setTimeout(refreshRem, 300);
}, false);
win.addEventListener('pageshow', function(e) {
if (e.persisted) {
clearTimeout(tid);
tid = setTimeout(refreshRem, 300);
}
}, false);

if (doc.readyState === 'complete') {
doc.body.style.fontSize = 12 * dpr + 'px';
} else {
doc.addEventListener('DOMContentLoaded', function(e) {
doc.body.style.fontSize = 12 * dpr + 'px';
}, false);
}


refreshRem();

flexible.dpr = win.dpr = dpr;
flexible.refreshRem = refreshRem;
flexible.rem2px = function(d) {
var val = parseFloat(d) * this.rem;
if (typeof d === 'string' && d.match(/rem$/)) {
val += 'px';
}
return val;
}
flexible.px2rem = function(d) {
var val = parseFloat(d) / this.rem;
if (typeof d === 'string' && d.match(/px$/)) {
val += 'rem';
}
return val;
}

})(window, window['lib'] || (window['lib'] = {}));

这种方式,可以精确地算出不同屏幕所应有的rem基准值,缺点就是要加载这么一段js代码,但个人觉得是这是目前最好的方案了。

因为这个方案同时解决了三个问题:

  1. border: 1px问题
  2. 图片高清问题
  3. 屏幕适配布局问题

字体大小问题

对于字体缩放问题,设计师原本的要求是这样的:任何手机屏幕上字体大小都要统一,所以我们针对不同的分辨率(dpr不同),会做如下处理:

1
2
3
4
font-size: 16px;
[data-dpr="2"] input {
font-size: 32px;
}

(注意,字体不可以用rem,误差太大了,且不能满足任何屏幕下字体大小相同)
为了方便,我们也会用less写一个mixin:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.px2px(@name, @px){
@{name}: round(@px / 2) * 1px;
[data-dpr="2"] & {
@{name}: @px * 1px;
}
// for mx3
[data-dpr="2.5"] & {
@{name}: round(@px * 2.5 / 2) * 1px;
}
// for 小米note
[data-dpr="2.75"] & {
@{name}: round(@px * 2.75 / 2) * 1px;
}
[data-dpr="3"] & {
@{name}: round(@px / 2 * 3) * 1px
}
// for 三星note4
[data-dpr="4"] & {
@{name}: @px * 2px;
}
}

(注意:html的data-dpr属性就是之前js方案里面有提到的,这里就有用处了)

根据经验和测试,还是会出现这些奇奇葩葩的dpr,这里做了统一兼容~

用的时候,就像这样:

1
.px2px(font-size, 32);

当然对于其他css属性,如果也要求不同dpr下都保持一致的话,也可以这样操作,如:

1
2
.px2px(padding, 20);
.px2px(right, 8);

  引入flexible.js是为了实现自适应,使移动端布局在不同设备上看起来是一样的,例如在iphone6上一行中可以看到3个div,那么在其他设备上也只能看到3个,通常来说,会让宽高paddingmargin等布局元素使用.px2rem进行转换,这样才能实现上述的效果。对于图片的设置,可以用.px2rem设置外层div宽高后,图片在里层100%;而文字通常来说用px来实现就好,这样在哪个设备上所看到的16px也就是真实的16px。

需要注意的问题是:
  当你要用到dpr和viewport等元素进行scale缩放时,才用到关于字体的.px2px(也就是说flexible.js和.px2px要同时使用,假设设计稿为750*1344,那么在less上写的宽高等也是按照750的设计稿来量,在网页调试状态下,所看到的宽高是在less中写的宽高,但是由于viewport的scale按照比例进行缩小了,所以才呈现了正常的逻辑像素下的效果,此处要理解!),因为这是专门为了设备的dpr不同而准备的,这里引申出来一个问题:
  如果你需要引入其他的css库,例如WeUI微信网页开发样式库,那么当你的页面使用scale进行缩放的时候,就会影响到其他库的使用,所以说,使用需三思。

  当然,你也可以摒弃一些关于dpr的设置,这样就不用加上字体的.px2px去适应不同的dpr了,而页面的可扩展性就更强。你只需要根据设计稿的大小加上.px2rem,并且设置好转换格式就能轻松地实现自适应屏幕了。
  最后给出一张没有布局适配(上图)和用rem布局适配(下图)的对比图,参考代码Github
没有布局适配
rem布局适配
  可以看出,同样的html代码,使用rem布局适配后无论在哪种移动端下,布局都是一样的,而没有使用布局适配的页面就会产生偏差:每一行的图标个数不同。

  如果忽略border: 1px问题这个问题的话,为了不影响其他样式库元素的缩放和方便你理解页面,我把js修改成如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
;(function(win, lib) {
var doc = win.document;
var docEl = doc.documentElement;
var tid;
var flexible = lib.flexible || (lib.flexible = {});

function refreshRem(){
var width = docEl.getBoundingClientRect().width;//逻辑像素
if (width > 540) {
width = 540;
}
var rem = width / 10;
docEl.style.fontSize = rem + 'px';
flexible.rem = win.rem = rem;
}

win.addEventListener('resize', function() {
clearTimeout(tid);
tid = setTimeout(refreshRem, 300);
}, false);
win.addEventListener('pageshow', function(e) {
//查看页面是直接从服务器上载入还是从缓存中读取,从浏览器的缓存中读取该属性返回 ture
if (e.persisted) {
clearTimeout(tid);
tid = setTimeout(refreshRem, 300);
}
}, false);

refreshRem();

flexible.refreshRem = refreshRem;
flexible.rem2px = function(d) {
var val = parseFloat(d) * this.rem;
if (typeof d === 'string' && d.match(/rem$/)) {
val += 'px';
}
return val;
}
flexible.px2rem = function(d) {
var val = parseFloat(d) / this.rem;
if (typeof d === 'string' && d.match(/px$/)) {
val += 'rem';
}
return val;
}

//var t1 = flexible.rem2px("10rem");
//var t2 = flexible.px2rem("375px");
//console.log(t1);
//console.log(t2);

})(window, window['lib'] || (window['lib'] = {}));

rem = document.documentElement.clientWidth / 10
上面说过的,乘以dpr,是因为页面有可能为了实现1px border页面会缩放(scale) 1/dpr 倍,所以这里可以不要dpr,这样就不需担心其他样式库的影响了。

总结

  一不小心又写了篇这么长的博客,耐心看完的一定是移动端真爱,答应你,下次写篇短一点的。那么,问题来了,看完后,新技能get到了吗?姐也只能帮你到这了。

参考文章:

  1. 移动端高清、多屏适配方案
  2. 一张图帮你看懂 iPhone 6 Plus 屏幕分辨率
  3. 浅谈前端移动端页面开发(布局篇)
  4. 移动前端
  5. 详解iPhone6分辨率与适配技巧
  6. RESOLUTION INDEPENDENCE Towards A Retina Web