NiYingfeng 的博客

记录技术、生活与思考

0%

随着 Babel 在各类开源项目中的使用,ES6 已经进入日常生活。在此简单回顾一下 ES6 语法概念。 箭头函数 =>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var foo = function(){return 1+1;};
//等价于
let foo = () => 1+1;

var foo = function(a, b){
if (a !== undefined && b !== undefined) {
return a + b
} else {
return 0
}
};
//等价于
var add = (a, b) => {
if (a !== undefined && b !== undefined) {
return a + b
} else {
return 0
}
}

let,const 与var具有函数级别作用域不同,let和const不同具有块级作用域,let定义变量,const定义常量。

1
2
3
4
5
6
var a = 1;
{
let a = 2;
console.log(a); //2
}
console.log(a); //1

解构赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
var value = [1, 2, 3, 4, 5];
var [el1, el2, el3] = value;

var value = [1, 2, [3, 4, 5]];
var [el1, el2, [el3, el4]] = value;

var person = {firstName: "John", lastName: "Doe"};
var {firstName, lastName} = person;

function findUser(userId, {includeProfile, includeHistory}) {
if (includeProfile) ...
if (includeHistory) ...
}

增强对象字面量

1
2
3
4
5
6
var worker = {
company: 'freelancer',
work() {
console.log('working...');
}
};

参数默认值

1
2
3
function findClosestShape( x=0, y=0 ){
// ...
}

余参

1
2
3
4
function format( pattern, ...params ){
// return params;
}
console.log( formart( 'a', 'b', 'c' ) );

class 增加类的概念

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Person {
constructor( nane ){
this.name = name;
}
describe(){
return "Person called" + this.name;
}
}
// 子类
class Employee extends Person{
constructor ( name, title ){
// super.constructor( name )
super( name );
this.title = title;
}
describe(){
// super.describe()
return super() + "(" + this.title + ")";
}
}

扩展操作符

1
2
3
4
5
let arr = [ -1, 7, 2 ];
let highest = Math.max( ...arr ); // 7
new Date( ...[ 2011, 11, 24 ] );
// 非破坏性的链接单个元素
let arr2 = [ ...arr, 9, -6 ]; // [ -1, 7, 2, 9, -6 ]

for-of 循环(对于所有遵守 ES6 迭代规则的对象均有效)

1
2
3
4
5
6
7
8
9
10
11
12
13
let arr = [ 'foo', 'bar', 'baz' ];
for( let element of arr ){
console.log( element );
}
// foo
// bar
// baz
for( let [ index, element ] of arr.entries() ){
console.log( index + "." + element );
}
// foo
// bar
// baz

模块化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// lib.js
export const sqet = Math.sqrt;
export function square( x ){
return x * x ;
}
export function diag( x, y ){
return sqrt( square(x) + square( y ) );
}


// main.js
import { square, diag } from 'lib';
console.log( square( 11 ) ); // 121
console.log( diag( 4, 3 ) ); // 5

模板字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
let str = String.raw`This is a text 
with multiple lines.
Escapes are not interpreted,
\n is not a newline.`

var parts = '/2012/10/Page.html'.match(XRegExp.rx`
^ # match at start of string only
/ (?<year> [^/]+ ) # capture top dir name as year
/ (?<month> [^/]+ ) # capture subdir name as month
/ (?<title> [^/]+ ) # capture base name as title
\.html? $ # .htm or .html file ext at end of path
`);
console.log( parts.year ); // 2012

标签提供关于HTML文档的元数据。元数据不会显示在页面上,但是对于机器是可读的。它可用于浏览器(如何显示内容或重新加载页面),搜索引擎(关键词),或其他 web 服务。 —— W3School

必选属性 可选属性

SEO优化

_页面关键词_:每个网页应具有描述该网页内容的一组唯一的关键字。

1
<meta name="keywords" content="站点关键词" />

_页面描述_:每个网页都应有一个不超过 150 个字符且能准确反映网页内容的描述标签。

1
<meta name="description" content="站点描述" />

_搜索引擎索引方式_:robotterms是一组使用逗号(,)分割的值,通常有如下几种取值:none,noindex,nofollow,all,index和follow

1
2
3
4
5
6
7
8
9
<meta name="robots" content="index,follow" />
<!--
all:文件将被检索,且页面上的链接可以被查询;
none:文件将不被检索,且页面上的链接不可以被查询;
index:文件将被检索;
follow:页面上的链接可以被查询;
noindex:文件将不被检索;
nofollow:页面上的链接不可以被查询。
-->

页面重定向和刷新 content内的数字代表时间(秒),既多少时间后刷新

1
<meta http-equiv="refresh" content="0;url=http://www.baidu.com" />

移动设备

_viewport_:能优化移动浏览器的显示。如果不是响应式网站,不要使用initial-scale或者禁用缩放。

1
<meta name="viewport" content="width=device-width, initial-scale=0,maximum-scale=0, user-scalable=no"/>
  • width:宽度(数值 / device-width)(范围从200 到10,000,默认为980 像素)
  • height:高度(数值 / device-height)(范围从223 到10,000)
  • initial-scale:初始的缩放比例 (范围从>0 到10)
  • minimum-scale:允许用户缩放到的最小比例
  • maximum-scale:允许用户缩放到的最大比例
  • user-scalable:用户是否可以手动缩 (no,yes)
  • minimal-ui:可以在页面加载时最小化上下状态栏。(已弃用)

忽略数字自动识别为电话号码

1
<meta content="telephone=no" name="format-detection" />

忽略识别邮箱

1
<meta content="email=no" name="format-detection" />

_添加智能 App 广告条 Smart App Banner_:告诉浏览器这个网站对应的app,并在页面上显示下载banner(如下图)。文档链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<meta name="apple-itunes-app" content="app-id=myAppStoreID, affiliate-data=myAffiliateData, app-argument=myURL">


<!-- 针对手持设备优化,主要是针对一些老的不识别viewport的浏览器,比如黑莓 -->
<meta name="HandheldFriendly" content="true">
<!-- 微软的老式浏览器 -->
<meta name="MobileOptimized" content="320">
<!-- uc强制竖屏 -->
<meta name="screen-orientation" content="portrait">
<!-- QQ强制竖屏 -->
<meta name="x5-orientation" content="portrait">
<!-- UC强制全屏 -->
<meta name="full-screen" content="yes">
<!-- QQ强制全屏 -->
<meta name="x5-fullscreen" content="true">
<!-- UC应用模式 -->
<meta name="browsermode" content="application">
<!-- QQ应用模式 -->
<meta name="x5-page-mode" content="app">
<!-- windows phone 点击无高光 -->
<meta name="msapplication-tap-highlight" content="no">

网页相关

申明编码

1
<meta charset='utf-8' />

优先使用 IE 最新版本和 Chrome

1
2
3
4
5
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<!-- 关于X-UA-Compatible -->
<meta http-equiv="X-UA-Compatible" content="IE=6" ><!-- 使用IE6 -->
<meta http-equiv="X-UA-Compatible" content="IE=7" ><!-- 使用IE7 -->
<meta http-equiv="X-UA-Compatible" content="IE=8" ><!-- 使用IE8 -->

_浏览器内核控制_:国内浏览器很多都是双内核(webkit和Trident),webkit内核高速浏览,IE内核兼容网页和旧版网站。而添加meta标签的网站可以控制浏览器选择何种内核渲染。

1
<meta name="renderer" content="webkit|ie-comp|ie-stand">

_禁止浏览器从本地计算机的缓存中访问页面内容_:这样设定,访问者将无法脱机浏览。

1
<meta http-equiv="Pragma" content="no-cache">

_转码申明_:用百度打开网页可能会对其进行转码(比如贴广告),避免转码可添加如下meta

1
<meta http-equiv="Cache-Control" content="no-siteapp" />

在PC的Web时代,网站跳转问题仅仅简化为一个HTTP地址。到了移动时代,APP之间的相互调起就变得复杂起来,各类系统下应用间跳转的底层技术都不一样,调用方式等均不一致。没有任何协会致力于解决这个问题,进行规范化的处理。

基础调用(Intent/Scheme)

前端方面使用Intent协议链接或者Scheme协议链接的形式,通过web view的窗口进行吊起,当然部分APP中会禁止该方式,比如微信,QQ等。 目前使用 scheme 调起情况简单总结(简单检测,更随版本环境而定):

安卓: 可正常直接调起:手百 搜狗 QQ 小米4自带浏览器 华为荣耀自带浏览器 需确认后可调起:UC,QQ浏览器,360浏览器 完全禁止:微信 chrome IOS: 可调起:手百,QQ浏览器,chrome,UC,360 (基本首次需确认是否调起) 完全禁止:微信 safari QQ (悲剧的是iOS10 对于外部scheme唤起全面禁用了 摔)

Android 和 iOS为了解决基础调用方式的复杂性依次推出了方便开发者得App Links技术。

  • 谷歌叫做App Links(Android 6.0)
  • 苹果叫做Universal Links(iOS9.0)

基本思想就是把打开应用的地址,统一为使用HTTP(S)方式,系统通过拦截和解析HOST地址,与系统注册的HOST进行匹配,如果发现就可以直接打开APP。 UniversalLinks 文档

iOS和Android系统上的webkit内核的浏览器以及嵌入在应用程序里面的webview中,两次连续“轻触”判断为“放大”的操作,所以在第一次被“轻触”后,浏览器仍需要等待判定是否具有第二次“轻触”操作,从而来决定是触发“放大”操作还是触发单击click事件,直接导致click时间具有300ms延迟的问题。

方案一

使用touchstart事件,该dom上触摸开始触发事件。

方案二

模拟快速点击方式,首先触发touchstart,并且在未触发touchmove的情况下,短时间内触发touchend,即算作快速点击事件,进行触发。如 zepto内的 tap事件,以及 fastclick

备注:touch事件会影响click事件,若touchend添加阻止默认事件,click事件则不会触发。 touchstart → touchmove → touchend touchstart → touchend → click

在retina显示屏下网页会由1px会被渲染为2px,那么视觉稿中1px的线条还原成网页需要css定义为.5px。大部分移动机型的css渲染不支持.5px,所以用了1px来替代,最终渲染为2px的粗线条,这个问题往往会被忽视,从而导致移动网页中普遍存在2px的粗线条。

原始方案

1
border: .5px solid #000;

方案一

通过transform:scale(x,y)来缩放线条为原来的一半,可显示0.5px的线条。

1
2
3
4
5
6
7
8
9
10
11
.item:after {
position: absolute;
content: '';
width: 100%;
left: 0;
bottom: 0;
height: 1px;
background-color: red;
-webkit-transform: scale(1,.5);
transform: scale(1,.5);
}

方案二

先定义1px的边框,拉伸内容的宽度和高度为父级的2倍,使用transform:scale(0.5)缩放为原来的0.5倍

1
2
-webkit-transform: scale(5);
transform: scale(5);

方案三

以下形式的图片实现,可以用 gif,png,或 base64 图片 image

1
2
3
4
.border{
border-width: 1px;
border-image: url(border.gif) 2 repeat;
}

方案四

原理:利用 box-shadow 向内收缩后的残余阴影

1
2
3
4
5
6
7
8
9
.item {
/* 模拟此效果:
border: 1px solid rgba(0, 0, 0, .15);
*/
box-shadow: 0 -1px 1px -1px rgba(0, 0, 0, .5),
-1px 0 1px -1px rgba(0, 0, 0, .5),
1px 0 1px -1px rgba(0, 0, 0, .5),
0 1px 1px -1px rgba(0, 0, 0, .5);
}

方案五

原理:利用背景色在1px中渐变的效果实现渐变模拟

1
2
3
4
5
6
7
8
9
10
.item:after {
content: "";
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 1px;
background-image: -webkit-linear-gradient(0deg,#e0e0e0 50%,transparent 50%);
background-image: linear-gradient(0deg,#e0e0e0 50%,transparent 50%);
}

方案六

通过 viewport + rem 实现的,在devicePixelRatio不同情况下输出不同 meta 的 viewpoint

1
<meta name="viewport" content="initial-scale=5, maximum-scale=5, minimum-scale=5, user-scalable=no">

推荐:

使用伪类+tranform的形式实现 0.5px!

Git 基础

  • Workspace:工作区
  • Index / Stage:暂存区
  • Repository:仓库区(或本地仓库)
  • Remote:远程仓库

Git 命令

一、新建代码库

1
2
3
4
5
$ git init ([project-name]) 
# 在当前目录新建一个Git代码库(新建一个目录,将其初始化为Git代码库)

$ git clone [url]
# 下载一个项目和它的整个代码历史

二、配置

Git的设置文件为.gitconfig,它可以在用户主目录下(全局配置),也可以在项目目录下(项目配置)。

1
2
3
4
5
$ git config –list
# 显示当前的Git配置

$ git config -e [--global]
# 编辑Git配置文件

三、增加/删除文件

1
2
3
4
5
6
7
8
$ git add ./[dir]/[file1] [file2] ...   
# 添加所有文件/指定目录/指定文件到暂存区

$ git rm [file1] [file2] ...
# 删除工作区文件,并且将这次删除放入暂存区

$ git mv [file-original] [file-renamed]
# 改名文件,并且将这个改名放入暂存区

四、代码提交

1
2
3
4
5
6
7
8
9
10
$ git commit -m [message]   # 提交暂存区到仓库区 
$ git commit [file1] [file2] ... -m [message] # 提交暂存区的指定文件到仓库区

$ git commit -a # 提交工作区自上次commit之后的变化,直接到仓库区
$ git commit -v # 提交时显示所有diff信息

$ git commit --amend -m [message]
# 使用一次新的commit,替代上一次提交,如果代码没有任何新变化,则用来改写上一次commit的提交信息
$ git commit --amend [file1] [file2] ...
# 重做上一次commit,并包括指定文件的新变化

五、分支

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
$ git branch /-r/-a 
# 列出所有本地分支/所有远程分支/所有本地分支和远程分支

$ git branch [branch-name]/[branch] [commit]
# 新建一个分支/新建一个分支 指向指定commit,但依然停留在当前分支

$ git checkout -b [branch]
# 新建一个分支,并切换到该分支

$ git branch --track [branch] [remote-branch]
# 新建一个分支,与指定的远程分支建立追踪关系

$ git checkout [branch-name]
# 切换到指定分支,并更新工作区

$ git branch --set-upstream [branch] [remote-branch]
# 建立追踪关系,在现有分支与指定的远程分支之间

$ git merge [branch]
# 合并指定分支到当前分支

$ git branch -d [branch-name]
# 删除分支

$ git push origin --delete [branch-name]
# 删除远程分支

六、标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ git tag       
# 列出所有tag
$ git tag [tag] ([commit])
# 新建一个tag在当前commit(在指定commit)
$ git tag -d [tag]
# 删除本地tag
$ git push origin :refs/tags/[tagName]
# 删除远程tag
$ git show [tag]
# 查看tag信息
$ git push [remote] [tag]
# 提交指定tag
$ git push [remote] --tags
# 提交所有tag
$ git checkout -b [branch] [tag]
# 新建一个分支,指向某个tag

七、查看信息

1
2
3
4
5
6
7
8
9
10
11
12
$ git status     
# 显示有变更的文件
$ git log
# 显示当前分支的版本历史
$ git log --stat
# 显示commit历史,以及每次commit发生变更的文件
$ git log -S [keyword]
# 搜索提交历史,根据关键词
$ git log [tag] HEAD --pretty=format:%s
# 显示某个commit之后的所有变动,每个commit占据一行
$ git log [tag] HEAD --grep feature
# 显示某个commit之后的所有变动,其"提交说明"必须符合搜索条件

八、远程同步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ git fetch [remote]        
# 下载远程仓库的所有变动
$ git remote -v
# 显示所有远程仓库
$ git remote show [remote]
# 显示某个远程仓库的信息
$ git remote add [shortname] [url]
# 增加一个新的远程仓库,并命名

$ git pull [remote] [branch]
# 取回远程仓库的变化,并与本地分支合并
$ git push [remote] [branch]
# 上传本地指定分支到远程仓库

$ git rebase
# 从上游分支获取最新commit信息,并有机的将当前分支和上游分支进行合并

九、撤销

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ git checkout [file]    
# 恢复暂存区的指定文件到工作区
$ git checkout [commit] [file]
# 恢复某个commit的指定文件到暂存区和工作区
$ git checkout .
# 恢复暂存区的所有文件到工作区

$ git reset [file]
# 重置暂存区的指定文件,与上一次commit保持一致,但工作区不变
$ git reset --hard
# 重置暂存区与工作区,与上一次commit保持一致
$ git reset [commit]

$ git revert [commit]
# 后者的所有变化都将被前者抵消,并且应用到当前分支

Git 命令图解

git reset

reset将一个分支的末端指向另一个提交。这可以用来移除当前分支的一些提交。git reset HEAD~2

git checkout

将HEAD移到一个新的分支

1
`git checkout Hotfix`

git revert

Revert撤销一个提交的同时会创建一个新的提交。这是一个安全的方法,因为它不会重写提交历史git revert HEAD~2

git rebase

基于已有提交的远程分支”origin”,创建一个叫”mywork”的分支 再在这个分支做一些修改,然后生成两个提交(commit),并且同时远程分支也有其他同学的提交,这时候 在此,一般我们都会用”pull”命令把”origin”分支上的修改拉下来与当前的分钟修改合并,其结果看起来就像一个新的”合并的提交”(merge commit) 如果使用 git rebase,会把你的”mywork”分支里的每个提交(commit)取消掉,并且把它们临时 保存为补丁(patch)(这些补丁放到”.git/rebase”目录中),然后把”mywork”分支更新 为最新的”origin”分支,最后把保存的这些补丁应用到”mywork”分支上

git merge –no-ff

–no-ff 指的是强行关闭fast-forward方式

Git 分支

Git 保存的不是文件差异或者变化量,而只是一系列文件快照 Git 仓库中有五个对象:三个表示文件快照内容的 blob 对象;一个记录着目录树内容及其中各个文件对应 blob 对象索引的 tree 对象;以及一个包含指向 tree 对象(根目录)的索引和其他提交信息元数据的 commit 对象。 作些修改后再次提交,那么这次的提交对象会包含一个指向上次提交对象的指针 Git 中的分支,其实本质上仅仅是个指向 commit 对象的可变指针。Git 会使用 master 作为分支的默认名字。在若干次提交后,你其实已经有了一个指向最后一次提交对象的 master 分支,它在每次提交的时候都会自动向前移动。 Git 是如何知道你当前在哪个分支上工作的呢?其实答案也很简单,它保存着一个名为 HEAD 的特别指针

Git 分支图解

1
$ git checkout -b iss53

1
2
$ vim a.js 
$ git commit -a -m 'added a new footer [issue 53]'

1
2
3
4
$ git checkout master
$ git checkout -b 'hotfix'
$ vim index.html
$ git commit -a -m 'fixed the broken email address'

1
2
$ git checkout master 
$ git merge hotfix

1
2
3
4
$ git branch -d hotfix
$ git checkout iss53
$ vim index.html
$ git commit -a -m 'finished the new footer [issue 53]'

1
2
$ git checkout master 
$ git merge iss53

Git 分支的艺术

1、 GIT在技术层面上,是一个无中心的分布式版本控制系统,但在管理层面上,建议保持一个中心版本库。 2、 建议一个中心版本库(origin)至少包括两个分支,即“主分支(master)”和“开发分支(develop)” 要确保:团队成员从主分支(master)获得的都是处于可发布状态的代码,而从开发分支(develop)应该总能够获得最新开发进展的代码。

3、在一个团队开发协作中,建议要有“辅助分支”的概念。 “辅助分支”,大体包括如下几类:“管理功能开发”的分支、“帮助构建可发布代码”的分支、“可以便捷的修复发布版本关键BUG”的分支。 “辅助分支”的最大特点就是“生命周期十分有限”,完成使命后即可被清除。

建议至少还应设置三类“辅助分支”,我们称之为“Feature branches”,“Release branches”,“Hotfix branches”。 4、 “Feature branches”,起源于develop分支,最终也会归于develop分支。

“Feature branches”常用于开发一个独立的新功能,且其最终的结局必然只有两个,其一是合并入“develop”分支,其二是被抛弃。最典型的“Fearture branches”一定是存在于团队开发者那里,而不应该是“中心版本库”中。

“Feature branches”起源于“develop”分支,实现方法是:

1
git checkout -b myfeature develop

“Feature branches”最终也归于“develop”分支,实现方式是:

1
2
3
4
git checkout devleop
git merge --no-ff myfeature(--no-ff,即not fast forward,其作用是:要求git merge即使在fast forward条件下也要产生一个新的merge commit)(采用--no-ff的方式进行分支合并,其目的在于,希望保持原有“Feature branches”整个提交链的完整性)
git branch -d myfeature
git push origin develop

5、“Release branch”,起源于develop分支,最终归于“develop”或“master”分支。这类分支建议命名为“release-*”

“Relase branch”通常负责“短期的发布前准备工作”、“小bug的修复工作”、“版本号等元信息的准备工作”。与此同时,“develop”分支又可以承接下一个新功能的开发工作了 “Release branch”产生新提交的最好时机是“develop”分支已经基本到达预期的状态,至少希望新功能已经完全从“Feature branches”合并到“develop”分支了

创建“Release branches”,方法是:

1
2
git checkout -b release-2 develop./bump-version.sh 2 (这个脚本用于将代码所有涉及版本信息的地方都统一修改到2,另外,需要用户根据自己的项目去编写适合的bump-version.sh)
git commit -a -m "Bumped version number to 2"

在一段短时间内,在“Release branches”上,我们可以继续修复bug。在此阶段,严禁新功能的并入,新功能应该是被合并到“develop”分支的。 经过若干bug修复后,“Release branches”上的代码已经达到可发布状态,此时,需要完成三个动作:

  • 第一是将“Release branches”合并到“master”分支
  • 第二是一定要为master上的这个新提交打TAG(记录里程碑)
  • 第三是要将“Release branches”合并回“develop”分支。 git checkout mastergit merge –no-ff release-1.2git tag -a 1.2(使用-u/-s/-a参数会创建tag对象,而非软tag) git checkout developgit merge –no-ff release-1.2git branch -d release-1.2

6、“Hotfix branches”源于“master”,归于“develop”或“master”,通常命名为“hotfix-*”

“Hotfix branches”类似于“Release branch”,但产生此分支总是非预期的关键BUG。

建议设立“Hotfix branches”的原因是:希望避免“develop分支”新功能的开发必须为BUG修复让路的情况。 建立“Hotfix branches”,方法是:

1
2
3
git checkout -b hotfix-1 master./bump-version.sh 1
git commit -a -m "Bumpt version to 1" (然后可以开始问题修复工作)
git commit -m "Fixed severe production problem" (在问题修复后,进行第二次提交)

BUG修复后,需要将“Hotfix branches”合并回“master”分支,同时也需要合并回“develop”分支,方法是:

1
2
3
4
git checkout mastergit merge --no-ff hotfix-1
git tag -a 1
git checkout developgit merge --no-ff hotfix-1
git branch -d hotfix-1

作者:腾讯Bugly 原文地址:原文地址

1. H5 缓存机制介绍

H5,即 HTML5,是新一代的 HTML 标准,加入很多新的特性。离线存储(也可称为缓存机制)是其中一个非常重要的特性。H5 引入的离线存储,这意味着 web 应用可进行缓存,并可在没有因特网连接时进行访问。 H5 应用程序缓存为应用带来三个优势:

  • 离线浏览 用户可在应用离线时使用它们
  • 速度 已缓存资源加载得更快
  • 减少服务器负载 浏览器将只从服务器下载更新过或更改过的资源。

根据标准,到目前为止,H5 一共有6种缓存机制,有些是之前已有,有些是 H5 才新加入的。

  • 浏览器缓存机制
  • Dom Storgage(Web Storage)存储机制
  • Web SQL Database 存储机制
  • Application Cache(AppCache)机制
  • Indexed Database (IndexedDB)
  • File System API

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

2. H5 缓存机制原理分析

2.1 浏览器缓存机制

浏览器缓存机制是指通过 HTTP 协议头里的 Cache-Control(或 Expires)和 Last-Modified(或 Etag)等字段来控制文件缓存的机制。这应该是 WEB 中最早的缓存机制了,是在 HTTP 协议中实现的,有点不同于 Dom Storage、AppCache 等缓存机制,但本质上是一样的。可以理解为,一个是协议层实现的,一个是应用层实现的。 Cache-Control 用于控制文件在本地缓存有效时长。最常见的,比如服务器回包:

1
Cache-Control:max-age=600

表示文件在本地应该缓存,且有效时长是600秒(从发出请求算起)。在接下来600秒内,如果有请求这个资源,浏览器不会发出 HTTP 请求,而是直接使用本地缓存的文件。 Last-Modified 是标识文件在服务器上的最新更新时间。下次请求时,如果文件缓存过期,浏览器通过 If-Modified-Since 字段带上这个时间,发送给服务器,由服务器比较时间戳来判断文件是否有修改。如果没有修改,服务器返回304告诉浏览器继续使用缓存;如果有修改,则返回200,同时返回最新的文件。 Cache-Control 通常与 Last-Modified 一起使用。一个用于控制缓存有效时间,一个在缓存失效后,向服务查询是否有更新。 Cache-Control 还有一个同功能的字段:Expires。Expires 的值一个绝对的时间点,如:Expires: Thu, 10 Nov 2015 08:45:11 GMT,表示在这个时间点之前,缓存都是有效的。 Expires 是 HTTP1.0 标准中的字段,Cache-Control 是 HTTP1.1 标准中新加的字段,功能一样,都是控制缓存的有效时间。当这两个字段同时出现时,Cache-Control 是高优化级的。 Etag 也是和 Last-Modified 一样,对文件进行标识的字段。不同的是,Etag 的取值是一个对文件进行标识的特征字串。在向服务器查询文件是否有更新时,浏览器通过 If-None-Match 字段把特征字串发送给服务器,由服务器和文件最新特征字串进行匹配,来判断文件是否有更新。没有更新回包304,有更新回包200。EtagLast-Modified 可根据需求使用一个或两个同时使用。两个同时使用时,只要满足基中一个条件,就认为文件没有更新。 另外有两种特殊的情况: 手动刷新页面(F5),浏览器会直接认为缓存已经过期(可能缓存还没有过期),在请求中加上字段:

1
Cache-Control:max-age=0

,发包向服务器查询是否有文件是否有更新。 强制刷新页面(Ctrl+F5),浏览器会直接忽略本地的缓存(有缓存也会认为本地没有缓存),在请求中加上字段:

1
Cache-Control:no-cache

(或 Pragma:no-cache),发包向服务重新拉取文件。 下面是通过 Google Chrome 浏览器(用其他浏览器+抓包工具也可以)自带的开发者工具,对一个资源文件不同情况请求与回包的截图。 首次请求:200 缓存有效期内请求:200(from cache) 缓存过期后请求:304(Not Modified) 一般浏览器会将缓存记录及缓存文件存在本地 Cache 文件夹中。Android 下 App 如果使用 Webview,缓存的文件记录及文件内容会存在当前 app 的 data 目录中。 分析:Cache-ControlLast-Modified 一般用在 Web 的静态资源文件上,如 JS、CSS 和一些图像文件。通过设置资源文件缓存属性,对提高资源文件加载速度,节省流量很有意义,特别是移动网络环境。但问题是:缓存有效时长该如何设置?如果设置太短,就起不到缓存的使用;如果设置的太长,在资源文件有更新时,浏览器如果有缓存,则不能及时取到最新的文件。 Last-Modified 需要向服务器发起查询请求,才能知道资源文件有没有更新。虽然服务器可能返回304告诉没有更新,但也还有一个请求的过程。对于移动网络,这个请求可能是比较耗时的。有一种说法叫“消灭304”,指的就是优化掉304的请求。 抓包发现,带 if-Modified-Since 字段的请求,如果服务器回包304,回包带有

1
Cache-Control:max-age

Expires 字段,文件的缓存有效时间会更新,就是文件的缓存会重新有效。304回包后如果再请求,则又直接使用缓存文件了,不再向服务器查询文件是否更新了,除非新的缓存时间再次过期。 另外,Cache-Control 与 Last-Modified 是浏览器内核的机制,一般都是标准的实现,不能更改或设置。以 QQ 浏览器的 X5为例,Cache-Control 与 Last-Modified 缓存不能禁用。缓存容量是12MB,不分HOST,过期的缓存会最先被清除。如果都没过期,应该优先清最早的缓存或最快到期的或文件大小最大的;过期缓存也有可能还是有效的,清除缓存会导致资源文件的重新拉取。 还有,浏览器,如 X5,在使用缓存文件时,是没有对缓存文件内容进行校验的,这样缓存文件内容被修改的可能。 分析发现,浏览器的缓存机制还不是非常完美的缓存机制。完美的缓存机制应该是这样的:

  • 缓存文件没更新,尽可能使用缓存,不用和服务器交互;
  • 缓存文件有更新时,第一时间能使用到新的文件;
  • 缓存的文件要保持完整性,不使用被修改过的缓存文件;
  • 缓存的容量大小要能设置或控制,缓存文件不能因为存储空间限制或过期被清除。

以X5为例,第1、2条不能同时满足,第3、4条都不能满足。 在实际应用中,为了解决 Cache-Control 缓存时长不好设置的问题,以及为了”消灭304“,Web前端采用的方式是:

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

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

2.2 Dom Storage 存储机制

DOM 存储是一套在 Web Applications 1.0 规范中首次引入的与存储相关的特性的总称,现在已经分离出来,单独发展成为独立的 W3C Web 存储规范。 DOM 存储被设计为用来提供一个更大存储量、更安全、更便捷的存储方法,从而可以代替掉将一些不需要让服务器知道的信息存储到 cookies 里的这种传统方法。 上面一段是对 Dom Storage 存储机制的官方表述。看起来,Dom Storage 机制类似 Cookies,但有一些优势。 Dom Storage 是通过存储字符串的 Key/Value 对来提供的,并提供 5MB (不同浏览器可能不同,分 HOST)的存储空间(Cookies 才 4KB)。另外 Dom Storage 存储的数据在本地,不像 Cookies,每次请求一次页面,Cookies 都会发送给服务器。 DOM Storage 分为 sessionStoragelocalStorage。localStorage 对象和 sessionStorage 对象使用方法基本相同,它们的区别在于作用的范围不同。sessionStorage 用来存储与页面相关的数据,它在页面关闭后无法使用。而 localStorage 则持久存在,在页面关闭后也可以使用。 Dom Storage 提供了以下的存储接口:

1
2
3
4
5
6
7
8
9
```
interface Storage {
readonly attribute unsigned long length;
[IndexGetter] DOMString key(in unsigned long index);
[NameGetter] DOMString getItem(in DOMString key);
[NameSetter] void setItem(in DOMString key, in DOMString data);
[NameDeleter] void removeItem(in DOMString key);
void clear();
};
1
2
3

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

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
<script type="text/javascript">
// 当页面刷新时,从sessionStorage恢复之前输入的内容
window.onload = function(){
if (window.sessionStorage) {
var name = window.sessionStorage.getItem("name");
if (name != "" || name != null){
document.getElementById("name").value = name;
}
}
};

// 将数据保存到sessionStorage对象中
function saveToStorage() {
if (window.sessionStorage) {
var name = document.getElementById("name").value;
window.sessionStorage.setItem("name", name);
window.location.href="session_storage.html";
}
}
</script>

<form action="./session_storage.html">
<input type="text" name="name" id="name"/>
<input type="button" value="Save" onclick="saveToStorage()"/>
</form>
1
2
3

当浏览器被意外刷新的时候,一些临时数据应当被保存和恢复。sessionStorage 对象在处理这种情况的时候是最有用的。比如恢复我们在表单中已经填写的数据。 把上面的代码复制到 session_storage.html(也可以从附件中直接下载)页面中,用 Google Chrome 浏览器的不同 PAGE 或 WINDOW 打开,在输入框中分别输入不同的文字,再点击“Save”,然后分别刷新。每个 PAGE 或 WINDOW 显示都是当前PAGE输入的内容,互不影响。关闭 PAGE,再重新打开,上一次输入保存的内容已经没有了。 ![](http://image.freefe.cc/20160815211746.png) `Local Storage` 的接口、用法与 `Session Storage` 一样,唯一不同的是:Local Storage 保存的数据是持久性的。当前 PAGE 关闭(Page Session 结束后),保存的数据依然存在。重新打开PAGE,上次保存的数据可以获取到。另外,Local Storage 是全局性的,同时打开两个 PAGE 会共享一份存数据,在一个PAGE中修改数据,另一个 PAGE 中是可以感知到的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
//通过localStorage直接引用key, 另一种写法,等价于:
//localStorage.getItem("pageLoadCount");
//localStorage.setItem("pageLoadCount", value);
if (!localStorage.pageLoadCount)
localStorage.pageLoadCount = 0;
localStorage.pageLoadCount = parseInt(localStorage.pageLoadCount) + 1;
document.getElementById('count').textContent = localStorage.pageLoadCount;
</script>

<p>
You have viewed this page
<span id="count">an untold number of</span>
time(s).
</p>
1
2
3

将上面代码复制到 local_storage.html 的页面中,用浏览器打开,pageLoadCount 的值是1;关闭 PAGE 重新打开,pageLoadCount 的值是2。这是因为第一次的值已经保存了。 ![](http://image.freefe.cc/20160815211804.png) 用两个 PAGE 同时打开 local_storage.html,并分别交替刷新,发现两个 PAGE 是共享一个 pageLoadCount 的。 ![](http://image.freefe.cc/20160815211818.png) 分析:Dom Storage 给 Web 提供了一种更录活的数据存储方式,存储空间更大(相对 Cookies),用法也比较简单,方便存储服务器或本地的一些临时数据。 从 DomStorage 提供的接口来看,DomStorage 适合存储比较简单的数据,如果要存储结构化的数据,可能要借助 JASON了,将要存储的对象转为 JASON 字串。不太适合存储比较复杂或存储空间要求比较大的数据,也不适合存储静态的文件等。 在 Android 内嵌 Webview 中,需要通过 Webview 设置接口启用 Dom Storage。

1
2
3
WebView myWebView = (WebView) findViewById(R.id.webview);
WebSettings webSettings = myWebView.getSettings();
webSettings.setDomStorageEnabled(true);
1
2
3
4
5
6
7

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

### 2.3 Web SQL Database存储机制

H5 也提供基于 SQL 的数据库存储机制,用于存储适合数据库的结构化数据。根据官方的标准文档,Web SQL Database 存储机制不再推荐使用,将来也不再维护,而是推荐使用 `AppCache` 和 `IndexedDB`。 现在主流的浏览器(点击查看浏览器支持情况)都还是支持 Web SQL Database 存储机制的。Web SQL Database 存储机制提供了一组 API 供 Web App 创建、存储、查询数据库。 下面通过简单的例子,演示下 Web SQL Database 的使用。

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
<script>
if(window.openDatabase){
//打开数据库,如果没有则创建
var db = openDatabase('mydb', '0', 'Test DB', 2 * 1024);

//通过事务,创建一个表,并添加两条记录
db.transaction(function (tx) {
tx.executeSql('CREATE TABLE IF NOT EXISTS LOGS (id unique, log)');
tx.executeSql('INSERT INTO LOGS (id, log) VALUES (1, "foobar")');
tx.executeSql('INSERT INTO LOGS (id, log) VALUES (2, "logmsg")');
});

//查询表中所有记录,并展示出来
db.transaction(function (tx) {
tx.executeSql('SELECT * FROM LOGS', [], function (tx, results) {
var len = results.rows.length, i;
msg = "<p>Found rows: " + len + "</p>";
for(i=0; i<len; i++){
msg += "<p>" + results.rows.item(i).log + "</p>";
}
document.querySelector('#status').innerHTML = msg;
}, null);
});
}

</script>

<div id="status" name="status">Status Message</div>
1
2
3

将上面代码复制到 sql_database.html 中,用浏览器打开,可看到下面的内容。 ![](http://image.freefe.cc/20160815211818.png) 官方建议浏览器在实现时,对每个 HOST 的数据库存储空间作一定限制,建议默认是 5MB(分 HOST)的配额;达到上限后,可以申请更多存储空间。另外,现在主流浏览器 SQL Database 的实现都是基于 SQLite。 分析:SQL Database 的主要优势在于能够存储结构复杂的数据,能充分利用数据库的优势,可方便对数据进行增加、删除、修改、查询。由于 SQL 语法的复杂性,使用起来麻烦一些。SQL Database 也不太适合做静态文件的缓存。 在 Android 内嵌 Webview 中,需要通过 Webview 设置接口启用 SQL Database,同时还要设置数据库文件的存储路径。

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

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

### 2.4 Application Cache 机制

Application Cache(简称 AppCache)似乎是为支持 Web App 离线使用而开发的缓存机制。它的缓存机制类似于浏览器的缓存(Cache-Control 和 Last-Modified)机制,都是以文件为单位进行缓存,且文件有一定更新机制。但 AppCache 是对浏览器缓存机制的补充,不是替代。 先拿 W3C 官方的一个例子,说下 AppCache 机制的用法与功能。

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html manifest="demo_html.appcache">
<body>

<script src="demo_time.js"></script>

<p id="timePara"><button onclick="getDateTime()">Get Date and Time</button></p>
<p><img src="img_logo.gif" width="336" height="69"></p>
<p>Try opening <a href="tryhtml5_html_manifest.htm" target="_blank">this page</a>, then go offline, and reload the page. The script and the image should still work.</p>

</body>
</html>
1
2
3
4
5
6
7
8
9

上面 HTML 文档,引用外部一个 JS 文件和一个 GIF 图片文件,在其 HTML 头中通过 manifest 属性引用了一个 appcache 结尾的文件。 我们在 Google Chrome 浏览器中打开这个 HTML 链接,JS 功能正常,图片也显示正常。禁用网络,关闭浏览器重新打开这个链接,发现 JS 工作正常,图片也显示正常。当然也有可能是浏览缓存起的作用,我们可以在文件的浏览器缓存过期后,禁用网络再试,发现 HTML 页面也是正常的。 通过 Google Chrome 浏览器自带的工具,我们可以查看已经缓存的 AppCache(分 HOST)。 ![](http://image.freefe.cc/20160815211900.png) 上面截图中的缓存,就是我们刚才打开 HTML 的页面 AppCache。从截图中看,HTML 页面及 HTML 引用的 JS、GIF 图像文件都被缓存了;另外 HTML 头中 manifest 属性引用的 appcache 文件也缓存了。 AppCache 的原理有两个关键点:manifest 属性和 manifest 文件。 HTML 在头中通过 manifest 属性引用 manifest 文件。manifest 文件,就是上面以 appcache 结尾的文件,是一个普通文件文件,列出了需要缓存的文件。 ![](http://image.freefe.cc/20160815211928.png) 上面截图中的 manifest 文件,就 HTML 代码引用的 manifest 文件。文件比较简单,第一行是关键字,第二、三行就是要缓存的文件路径(相对路径)。这只是最简单的 manifest 文件,完整的还包括其他关键字与内容。引用 manifest 文件的 HTML 和 manifest 文件中列出的要缓存的文件最终都会被浏览器缓存。 完整的 manifest 文件,包括三个 Section,类型 Windows 中 ini 配置文件的 Section,不过不要中括号。

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

完整的 manifest 文件,如:

1
2
3
4
5
6
7
8
9
10
11
CACHE MANIFEST
# 2012-02-21 v0
/theme.css
/logo.gif
/main.js

NETWORK:
login.asp

FALLBACK:
/html/ /offline.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14

总的来说,浏览器在首次加载 HTML 文件时,会解析 manifest 属性,并读取 manifest 文件,获取 Section:CACHE MANIFEST 下要缓存的文件列表,再对文件缓存。 AppCache 的缓存文件,与浏览器的缓存文件分开存储的,还是一份?应该是分开的。因为 AppCache 在本地也有 5MB(分 HOST)的空间限制。 AppCache 在首次加载生成后,也有更新机制。被缓存的文件如果要更新,需要更新 manifest 文件。因为浏览器在下次加载时,除了会默认使用缓存外,还会在后台检查 manifest 文件有没有修改(byte by byte)。发现有修改,就会重新获取 manifest 文件,对 Section:CACHE MANIFEST 下文件列表检查更新。manifest 文件与缓存文件的检查更新也遵守浏览器缓存机制。 如用用户手动清了 AppCache 缓存,下次加载时,浏览器会重新生成缓存,也可算是一种缓存的更新。另外, Web App 也可用代码实现缓存更新。 分析:AppCache 看起来是一种比较好的缓存方法,除了缓存静态资源文件外,也适合构建 Web 离线 App。在实际使用中有些需要注意的地方,有一些可以说是”坑“。

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

另外,根据官方文档,AppCache 已经不推荐使用了,标准也不会再支持。现在主流的浏览器都是还支持 AppCache的,以后就不太确定了。 在Android 内嵌 Webview中,需要通过 Webview 设置接口启用 AppCache,同时还要设置缓存文件的存储路径,另外还可以设置缓存的空间大小。

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

### 2.5 Indexed Database

IndexedDB 也是一种数据库的存储机制,但不同于已经不再支持的 Web SQL Database。IndexedDB 不是传统的关系数据库,可归为 NoSQL 数据库。IndexedDB 又类似于 Dom Storage 的 key-value 的存储方式,但功能更强大,且存储空间更大。 IndexedDB 存储数据是 key-value 的形式。Key 是必需,且要唯一;Key 可以自己定义,也可由系统自动生成。Value 也是必需的,但 Value 非常灵活,可以是任何类型的对象。一般 Value 都是通过 Key 来存取的。 IndexedDB 提供了一组 API,可以进行数据存、取以及遍历。这些 API 都是异步的,操作的结果都是在回调中返回。 下面代码演示了 IndexedDB 中 DB 的打开(创建)、存储对象(可理解成有关系数据的”表“)的创建及数据存取、遍历基本功能。

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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
<script type="text/javascript">
var db;
window.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
//浏览器是否支持IndexedDB
if (window.indexedDB) {
//打开数据库,如果没有,则创建
var openRequest = window.indexedDB.open("people_db", 1);
//DB版本设置或升级时回调
openRequest.onupgradeneeded = function(e) {
console.log("Upgrading...");
var thisDB = e.target.result;
if(!thisDB.objectStoreNames.contains("people")) {
console.log("Create Object Store: people.");
//创建存储对象,类似于关系数据库的表
thisDB.createObjectStore("people", { autoIncrement:true });
//创建存储对象, 还创建索引
//var objectStore = thisDB.createObjectStore("people",{ autoIncrement:true });
// //first arg is name of index, second is the path (col);
//objectStore.createIndex("name","name", {unique:false});
//objectStore.createIndex("email","email", {unique:true});
}
}

//DB成功打开回调
openRequest.onsuccess = function(e) {
console.log("Success!");
//保存全局的数据库对象,后面会用到
db = e.target.result;
//绑定按钮点击事件
document.querySelector("#addButton").addEventListener("click", addPerson, false);

document.querySelector("#getButton").addEventListener("click", getPerson, false);

document.querySelector("#getAllButton").addEventListener("click", getPeople, false);

document.querySelector("#getByName").addEventListener("click", getPeopleByNameIndex1, false);
}
//DB打开失败回调
openRequest.onerror = function(e) {
console.log("Error");
console.dir(e);
}
}else{
alert('Sorry! Your browser doesn\'t support the IndexedDB.');
}

//添加一条记录
function addPerson(e) {
var name = document.querySelector("#name").value;
var email = document.querySelector("#email").value;
console.log("About to add "+name+"/"+email);
var transaction = db.transaction(["people"],"readwrite");
var store = transaction.objectStore("people");
//Define a person
var person = {
name:name,
email:email,
created:new Date()
}
//Perform the add
var request = store.add(person);
//var request = store.put(person, 2);
request.onerror = function(e) {
console.log("Error",e.target.error.name);
//some type of error handler
}
request.onsuccess = function(e) {
console.log("Woot! Did it.");
}
}
//通过KEY查询记录
function getPerson(e) {
var key = document.querySelector("#key").value;
if(key === "" || isNaN(key)) return;
var transaction = db.transaction(["people"],"readonly");
var store = transaction.objectStore("people");
var request = store.get(Number(key));
request.onsuccess = function(e) {
var result = e.target.result;
console.dir(result);
if(result) {
var s = "<p><h2>Key "+key+"</h2></p>";
for(var field in result) {
s+= field+"="+result[field]+"<br/>";
}
document.querySelector("#status").innerHTML = s;
} else {
document.querySelector("#status").innerHTML = "<h2>No match!</h2>";
}
}
}
//获取所有记录
function getPeople(e) {
var s = "";
db.transaction(["people"], "readonly").objectStore("people").openCursor().onsuccess = function(e) {
var cursor = e.target.result;
if(cursor) {
s += "<p><h2>Key "+cursor.key+"</h2></p>";
for(var field in cursor.value) {
s+= field+"="+cursor.value[field]+"<br/>";
}
s+="</p>";
cursor.continue();
}
document.querySelector("#status2").innerHTML = s;
}
}
//通过索引查询记录
function getPeopleByNameIndex(e)
{
var name = document.querySelector("#name1").value;

var transaction = db.transaction(["people"],"readonly");
var store = transaction.objectStore("people");
var index = store.index("name");

//name is some value
var request = index.get(name);

request.onsuccess = function(e) {
var result = e.target.result;
if(result) {
var s = "<p><h2>Name "+name+"</h2><p>";
for(var field in result) {
s+= field+"="+result[field]+"<br/>";
}
s+="</p>";
} else {
document.querySelector("#status3").innerHTML = "<h2>No match!</h2>";
}
}
}
//通过索引查询记录
function getPeopleByNameIndex1(e)
{
var s = "";
var name = document.querySelector("#name1").value;
var transaction = db.transaction(["people"],"readonly");
var store = transaction.objectStore("people");
var index = store.index("name");
//name is some value
index.openCursor().onsuccess = function(e) {
var cursor = e.target.result;
if(cursor) {
s += "<p><h2>Key "+cursor.key+"</h2></p>";
for(var field in cursor.value) {
s+= field+"="+cursor.value[field]+"<br/>";
}
s+="</p>";
cursor.continue();
}
document.querySelector("#status3").innerHTML = s;
}
}
</script>
<p>添加数据<br/>
<input type="text" id="name" placeholder="Name"><br/>
<input type="email" id="email" placeholder="Email"><br/>
<button id="addButton">Add Data</button>
</p>
<p>根据Key查询数据<br/>
<input type="text" id="key" placeholder="Key"><br/>
<button id="getButton">Get Data</button>
</p>
<div id="status" name="status"></div>
<p>获取所有数据<br/>
<button id="getAllButton">Get EveryOne</button>
</p>
<div id="status2" name="status2"></div>
<p>根据索引:Name查询数据<br/>
<input type="text" id="name1" placeholder="Name"><br/>
<button id="getByName">Get ByName</button>
</p>
<div id="status3" name="status3"></div>
1
2
3

将上面的代码复制到 indexed_db.html 中,用 Google Chrome 浏览器打开,就可以添加、查询数据。在 Chrome 的开发者工具中,能查看创建的 DB 、存储对象(可理解成表)以及表中添加的数据。 ![](http://image.freefe.cc/20160815211928.png) IndexedDB 有个非常强大的功能,就是 index(索引)。它可对 Value 对象中任何属性生成索引,然后可以基于索引进行 Value 对象的快速查询。 要生成索引或支持索引查询数据,需求在首次生成存储对象时,调用接口生成属性的索引。可以同时对对象的多个不同属性创建索引。如下面代码就对name 和 email 两个属性都生成了索引。

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function getPeopleByNameIndex(e){
var name = document.querySelector("#name1").value;
var transaction = db.transaction(["people"],"readonly");
var store = transaction.objectStore("people");
var index = store.index("name");
//name is some value
var request = index.get(name);
request.onsuccess = function(e) {
var result = e.target.result;
if(result) {
var s = "<p><h2>Name "+name+"</h2><p>";
for(var field in result) {
s+= field+"="+result[field]+"<br/>";
}
s+="</p>";
} else {
document.querySelector("#status3").innerHTML = "<h2>No match!</h2>";
}
}
}
1
2
3
4
5
6
7
8
9
10
11

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

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

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

1
2
3
WebView myWebView = (WebView) findViewById(R.id.webview);
WebSettings webSettings = myWebView.getSettings();
webSettings.setJavaScriptEnabled(true);
1
2
3
4
5
6
7
8
9
10
11

### 2.6 File System API

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

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

浏览器给虚拟文件系统提供了两种类型的存储空间:临时的和持久性的。临时的存储空间是由浏览器自动分配的,但可能被浏览器回收;持久性的存储空间需要显示的申请,申请时浏览器会给用户一提示,需要用户进行确认。持久性的存储空间是 WebApp 自己管理,浏览器不会回收,也不会清除内容。持久性的存储空间大小是通过配额来管理的,首次申请时会一个初始的配额,配额用完需要再次申请。 虚拟的文件系统是运行在沙盒中。不同 WebApp 的虚拟文件系统是互相隔离的,虚拟文件系统与本地文件系统也是互相隔离的。 File System API 提供了一组文件与文件夹的操作接口,有同步和异步两个版本,可满足不同的使用场景。下面通过一个文件创建、读、写的例子,演示下简单的功能与用法。

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
<script type="text/javascript">
window.requestFileSystem = window.requestFileSystem || window.webkitRequestFileSystem;
//请求临时文件的存储空间
if (window.requestFileSystem) {
window.requestFileSystem(window.TEMPORARY, 5*1024*1024, initFS, errorHandler);
}else{
alert('Sorry! Your browser doesn\'t support the FileSystem API');
}
//请求成功回调
function initFS(fs){
//在根目录下打开log.txt文件,如果不存在就创建
//fs就是成功返回的文件系统对象,fs.root代表根目录
fs.root.getFile('log.txt', {create: true}, function(fileEntry) {
//fileEntry是返回的一个文件对象,代表打开的文件
//向文件写入指定内容
writeFile(fileEntry);
//将写入的内容又读出来,显示在页面上
readFile(fileEntry);
}, errorHandler);
}
//读取文件内容
function readFile(fileEntry)
{
console.log('readFile');

// Get a File object representing the file,
// then use FileReader to read its contents.
fileEntry.file(function(file) {
console.log('createReader');
var reader = new FileReader();
reader.onloadend = function(e) {
console.log('onloadend');
var txtArea = document.createElement('textarea');
txtArea.value = this.result;
document.body.appendChild(txtArea);
};
reader.readAsText(file);
}, errorHandler);
}

//向文件写入指定内容
function writeFile(fileEntry)
{
console.log('writeFile');
// Create a FileWriter object for our FileEntry (log.txt).
fileEntry.createWriter(function(fileWriter) {
console.log('createWriter');
fileWriter.onwriteend = function(e) {
console.log('Write completed');
};
fileWriter.onerror = function(e) {
console.log('Write failed: ' + e.toString());
};
// Create a new Blob and write it to log.txt.
var blob = new Blob(['Hello, World!'], {type: 'text/plain'});
fileWriter.write(blob);
}, errorHandler);
}
function errorHandler(err){
var msg = 'An error occured: ' + err;
console.log(msg);
};
</script>

```

将上面代码复制到 file_system_api.html 文件中,用 Google Chrome 浏览器打开(现在 File System API 只有 Chrome 43+、Opera 32+ 以及 Chrome for Android 46+ 这三个浏览器支持)。由于 Google Chrome 禁用了本地 HTML 文件中的 File System API功能,在启动 Chrome 时,要加上”—allow-file-access-from-files“命令行参数。 上面截图,左边是 HTML 运行的结果,右边是 Chrome 开发者工具中看到的 Web 的文件系统。基本上 H5的几种缓存机制的数据都能在这个开发者工具看到,非常方便。 分析:File System API 给 Web App 带来了文件系统的功能,Native 文件系统的功能在 Web App 中都有相应的实现。任何需要通过文件来管理数据,或通过文件系统进行数据管理的场景都比较适合。 到目前,Android 系统的 Webview 还不支持 File System API。

3 移动端 Web 加载性能(缓存)优化

分析完 H5提供的各种缓存机制,回到移动端(针对 Android,可能也适用于 iOS)的场景。现在 Android App(包括手 Q 和 WX)大多嵌入了 Webview 的组件(系统 Webview 或 QQ 游览器的 X5组件),通过内嵌Webview 来加载一些H5的运营活动页面或资讯页。这样可充分发挥Web前端的优势:快速开发、发布,灵活上下线。但 Webview 也有一些不可忽视的问题,比较突出的就是加载相对较慢,会相对消耗较多流量。 通过对一些 H5页面进行调试及抓包发现,每次加载一个 H5页面,都会有较多的请求。除了 HTML 主 URL 自身的请求外,HTML外部引用的 JS、CSS、字体文件、图片都是一个独立的 HTTP 请求,每一个请求都串行的(可能有连接复用)。这么多请求串起来,再加上浏览器解析、渲染的时间,Web 整体的加载时间变得较长;请求文件越多,消耗的流量也会越多。我们可综合使用上面说到几种缓存机制,来帮助我们优化 Web 的加载性能。 结论:综合各种缓存机制比较,对于静态文件,如 JS、CSS、字体、图片等,适合通过浏览器缓存机制来进行缓存,通过缓存文件可大幅提升 Web 的加载速度,且节省流量。但也有一些不足:缓存文件需要首次加载后才会产生;浏览器缓存的存储空间有限,缓存有被清除的可能;缓存的文件没有校验。要解决这些不足,可以参考手 Q 的离线包,它有效的解决了这些不足。 对于 Web 在本地或服务器获取的数据,可以通过 Dom Storage 和 IndexedDB 进行缓存。也在一定程度上减少和 Server 的交互,提高加载速度,同时节省流量。 当然 Web 的性能优化,还包括选择合适的图片大小,避免 JS 和 CSS 造成的阻塞等。这就需要 Web 前端的同事根据一些规范和一些调试工具进行优化了。

通过网络获取内容既缓慢,成本又高:大的响应需要在客户端和服务器之间进行多次往返通信,这拖延了浏览器可以使用和处理内容的时间,同时也增加了访问者的数据成本。因此,缓存和重用以前获取的资源的能力成为优化性能很关键的一个方面。

Contents 使用 ETag 验证缓存的响应 Cache-Control 定义最优 Cache-Control 策略 废弃和更新已缓存的响应 缓存检查表

好消息是每个浏览器都实现了 HTTP 缓存! 我们所要做的就是,确保每个服务器响应都提供正确的 HTTP 头指令,以指导浏览器何时可以缓存响应以及可以缓存多久。

如果在应用中使用 Webview 来获取和显示网页内容,可能需要提供额外的配置标志,以确保启用了 HTTP 缓存,并根据用途设置了合理的缓存大小,同时,确保缓存持久化。查看平台文档并确认您的设置!

服务器在返回响应时,还会发出一组 HTTP 头,用来描述内容类型、长度、缓存指令、验证令牌等。例如,在上图的交互中,服务器返回了一个 1024 字节的响应,指导客户端缓存响应长达 120 秒,并提供验证令牌(x234dff),在响应过期之后,可以用来验证资源是否被修改。

使用 ETag 验证缓存的响应

TL;DR 服务器通过 ETag HTTP 头传递验证令牌 通过验证令牌可以进行高效的资源更新检查:如果资源未更改,则不会传输任何数据。

让我们假设在首次获取资源 120 秒之后,浏览器又对该资源发起了新请求。首先,浏览器会检查本地缓存并找到之前的响应,不幸的是,这个响应现在已经’过期’,无法在使用。此时,浏览器也可以直接发出新请求,获取新的完整响应,但是这样做效率较低,因为如果资源未被更改过,我们就没有理由再去下载与缓存中已有的完全相同的字节。 这就是 ETag 头中指定的验证令牌所要解决的问题:服务器会生成并返回一个随机令牌,通常是文件内容的哈希值或者某个其他指纹码。客户端不必了解指纹码是如何生成的,只需要在下一个请求中将其发送给服务器:如果指纹码仍然一致,说明资源未被修改,我们就可以跳过下载。 在上面的例子中,客户端自动在If-None-MatchHTTP 请求头中提供 ETag 令牌,服务器针对当前的资源检查令牌,如果未被修改过,则返回304 Not Modified响应,告诉浏览器缓存中的响应未被修改过,可以再延用 120 秒。注意,我们不必再次下载响应 - 这节约了时间和带宽。 作为网络开发人员,您如何利用高效的重新验证? 浏览器代替我们完成了所有的工作:自动检测是否已指定了验证令牌,并会将验证令牌附加到发出的请求上,根据从服务器收到的响应,在必要时更新缓存时间戳。实际上,我们唯一要做的就是确保服务器提供必要的 ETag 令牌:查看服务器文档中是否有必要的配置标志。

注意 提示:HTML5 Boilerplate 项目包含了所有最流行的服务器的配置文件样例,并且为每个配置标志和设置都提供了详细的注释:在列表中找到您喜欢的服务器,查找适合的设置,然后复制/确认您的服务器配置了推荐的设置。

Cache-Control

TL;DR 每个资源都可以通过 Cache-Control HTTP 头来定义自己的缓存策略 Cache-Control 指令控制谁在什么条件下可以缓存响应以及可以缓存多久

最好的请求是不必与服务器进行通信的请求:通过响应的本地副本,我们可以避免所有的网络延迟以及数据传输的数据成本。为此,HTTP 规范允许服务器返回 一系列不同的 Cache-Control 指令,控制浏览器或者其他中继缓存如何缓存某个响应以及缓存多长时间。

注意 Cache-Control 头在 HTTP/1.1 规范中定义,取代了之前用来定义响应缓存策略的头(例如 Expires)。当前的所有浏览器都支持 Cache-Control,因此,使用它就够了。

no-cache 和 no-store no-cache表示必须先与服务器确认返回的响应是否被更改,然后才能使用该响应来满足后续对同一个网址的请求。因此,如果存在合适的验证令牌 (ETag),no-cache 会发起往返通信来验证缓存的响应,如果资源未被更改,可以避免下载。 相比之下,no-store更加简单,直接禁止浏览器和所有中继缓存存储返回的任何版本的响应 - 例如:一个包含个人隐私数据或银行数据的响应。每次用户请求该资源时,都会向服务器发送一个请求,每次都会下载完整的响应。 public和 private 如果响应被标记为public,即使有关联的 HTTP 认证,甚至响应状态码无法正常缓存,响应也可以被缓存。大多数情况下,public不是必须的,因为明确的缓存信息(例如max-age)已表示 响应可以被缓存。 相比之下,浏览器可以缓存private响应,但是通常只为单个用户缓存,因此,不允许任何中继缓存对其进行缓存 - 例如,用户浏览器可以缓存包含用户私人信息的 HTML 网页,但是 CDN 不能缓存。 max-age 该指令指定从当前请求开始,允许获取的响应被重用的最长时间(单位为秒) - 例如:max-age=60表示响应可以再缓存和重用 60 秒。

定义最优 Cache-Control 策略

按照上面的决策树来确定您的应用使用的特定资源或一组资源的最优缓存策略。理想情况下,目标应该是在客户端上缓存尽可能多的响应、缓存尽可能长的时间,并且为每个响应提供验证令牌,以便进行高效的重新验证。 根据 HTTP Archive,在排名最高的 300,000 个网站中(Alexa 排名),所有下载的响应中,几乎有半数可以由浏览器进行缓存,对于重复性网页浏览和访问来说,这是一个巨大的节省! 当然,这并不意味着特定的应用会有 50% 的资源可以被缓存:有些网站可以缓存 90% 以上的资源, 而有些网站有许多私密的或者时间要求苛刻的数据,根本无法被缓存。 审查您的网页,确定哪些资源可以被缓存,并确保可以返回正确的 Cache-Control 和 ETag 头。

废弃和更新已缓存的响应

TL;DR 在资源”过期”之前,将一直使用本地缓存的响应 通过将文件内容指纹码嵌入网址,我们可以强制客户端更新到新版的响应 为了获得最佳性能,每个应用需要定义自己的缓存层级

浏览器发出的所有 HTTP 请求会首先被路由到浏览器的缓存,以查看是否缓存了可以用于实现请求的有效响应。如果有匹配的响应,会直接从缓存中读取响应,这样就避免了网络延迟以及传输产生的数据成本。然而,如果我们希望更新或废弃已缓存的响应,该怎么办? 例如,假设我们已经告诉访问者某个 CSS 样式表缓存长达 24 小时 (max-age=86400),但是设计人员刚刚提交了一个更新,我们希望所有用户都能使用。我们该如何通知所有访问者缓存的 CSS 副本已过时,需要更新缓存? 这是一个欺骗性的问题 - 实际上,至少在不更改资源网址的情况下,我们做不到。 一旦浏览器缓存了响应,在过期以前,将一直使用缓存的版本,这是由 max-age 或者 expires 指定的,或者直到因为某些原因从缓存中删除,例如用户清除了浏览器缓存。因此,在构建网页时,不同的用户可能使用的是文件的不同版本;刚获取该资源的用户将使用新版本,而缓存过之前副本(但是依然有效)的用户将继续使用旧版本的响应。 所以,我们如何才能鱼和熊掌兼得:客户端缓存和快速更新? 很简单,在资源内容更改时,我们可以更改资源的网址,强制用户下载新响应。通常情况下,可以通过在文件名中嵌入文件的指纹码(或版本号)来实现 - 例如 style.x234dff.css。 因为能够定义每个资源的缓存策略,所以,我们可以定义’缓存层级’,这样,不但可以控制每个响应的缓存时间,还可以控制访问者看到新版本的速度。例如,我们一起分析一下上面的例子:

  • HTML 被标记成no-cache,这意味着浏览器在每次请求时都会重新验证文档,如果内容更改,会获取最新版本。同时,在 HTML 标记中,我们在 CSS 和 JavaScript 资源的网址中嵌入指纹码:如果这些文件的内容更改,网页的 HTML 也会随之更改,并将下载 HTML 响应的新副本。
  • 允许浏览器和中继缓存(例如 CDN)缓存 CSS,过期时间设置为 1 年。注意,我们可以放心地使用 1 年的’远期过期’,因为我们在文件名中嵌入了文件指纹码:如果 CSS 更新,网址也会随之更改。
  • JavaScript 过期时间也设置为 1 年,但是被标记为 private,也许是因为包含了 CDN 不应缓存的一些用户私人数据。
  • 缓存图片时不包含版本或唯一指纹码,过期时间设置为 1 天。

组合使用 ETag、Cache-Control 和唯一网址,我们可以提供最佳的方案:较长的过期时间,控制可以缓存响应的位置,以及按需更新。

缓存检查表

不存在最佳的缓存策略。根据您的通信模式、提供的数据类型以及应用特定的数据更新要求,必须定义和配置每个资源最适合的设置以及整体的’缓存层级’。 在定义缓存策略时,要记住下列技巧和方法:

  • 使用一致的网址:如果您在不同的网址上提供相同的内容,将会多次获取和存储该内容。提示:注意,网址区分大小写!
  • 确保服务器提供验证令牌 (ETag):通过验证令牌,如果服务器上的资源未被更改,就不必传输相同的字节。
  • 确定中继缓存可以缓存哪些资源:对所有用户的响应完全相同的资源很适合由 CDN 或其他中继缓存进行缓存。
  • 确定每个资源的最优缓存周期:不同的资源可能有不同的更新要求。审查并确定每个资源适合的 max-age。
  • 确定网站的最佳缓存层级:对 HTML 文档组合使用包含内容指纹码的资源网址以及短时间或 no-cache 的生命周期,可以控制客户端获取更新的速度。
  • 搅动最小化:有些资源的更新比其他资源频繁。如果资源的特定部分(例如 JavaScript 函数或一组 CSS 样式)会经常更新,应考虑将其代码作为单独的文件提供。这样,每次获取更新时,剩余内容(例如不会频繁更新的库代码)可以从缓存中获取,确保下载的内容量最少。

作者 Profile photo of Ilya Grigorik Ilya Grigorik Ilya is a Developer Advocate and Web Perf Guru

那一缕白是什么情况

有时候会经常遇到这种情况,某一个元素内部放置一张图片,可是偏偏就是它,图片底部怎么得也不和外层元素的底部重合,留出一缕散发出蛋蛋的忧伤的那么些像素! 有经验的刷刷刷添加几个样式就规避掉了,新手就干瞪眼着瞎尝试。对于其本质原因也都忽视了。

细聊一下vertical-align

对于vertical-align相对于text-align来说接触会少很多,很大一个原因是我们对他不甚了解,导致很所时候需要该样式来处理的一些样式,我们宁可绕过去使用其他方式进行解决。 vertical-align 为定义行内元素的基线相对于元素所在行的基线的垂直对齐方式。 先简单来看下vertical-align的值表

描述
baseline 默认。元素放置在父元素的基线上。
sub 垂直对齐文本的下标。
super 垂直对齐文本的上标
top 把元素的顶端与行中最高元素的顶端对齐
text-top 把元素的顶端与父元素字体的顶端对齐
middle 把此元素放置在父元素的中部。
bottom 把元素的顶端与行中最低的元素的顶端对齐。
text-bottom 把元素的底端与父元素字体的底端对齐。
length
% 使用 “line-height” 属性的百分比值来排列此元素。允许使用负值。
inherit 规定应该从父元素继承 vertical-align 属性的值。

对应类似刚刚学习英文时候的英文练习本: 看下图,只要这边为设置 img vertical-align 小实例(fg只是为了观察字母上下边界): 需要注意,这边我们设置的为 image的 vertical-align 的值。

原来如此

然后应该比较容易的理解,最上图出现的那一缕不和谐:image标签为行内元素,直接导致其默认的垂直对齐方式为-baseline,故而图片的下底边对应了外层div的基线baseline,而baseline并不是外层元素的正真的底部(在未设置font-size为0时),而是高于底部的一条基线,所以,那一块空白区域就由此而生。 那么解决方案也就明白了:

  1. 设置image 的垂直对齐方式为: top, bottom, middle 等
  2. 设置image display:block; 消除vertical-align 影响
  3. 设置父级元素,font-size:0; 或者 line-height:0;(不推荐)

行内元素之间的空白

顺便也提起一下关于多个行内元素之前的空白

1
2
3
4
5
6
<div class="container">
<span>1234</span>
<span>1234</span>
<span>1234</span>
<span>1234</span>
</div>

其实是由于空白元素造成的,起最好的解决方案为

1
2
3
4
.container span:after{
content: ' ';
font-size:0;
}

你想要处理以下问题时

  1. 两个块级元素上下元素 margin-bottom 与 margin-top 重叠。
  2. 父级元素未将第一个子元素的margin-top包含在内部。
  3. 图文混排中,文字排列会绕着浮动的图片进行排列,而不是左右分块。
  4. 父级元素高度无法被浮动的子元素撑开。

然后我们就使用一系列”网上教程”进行解决~

BFC是啥

BFC 意为 block formatting contexts,块级格式化上下文。 w3c的定义如下:

浮动元素,绝对定位元素,非块级盒模型的块级容器(比如 inline-block,table-cell以及table-caption的),以及 overflow 不为 visible 的块级盒模型 (除非该值已经被渲染到可视区域)均会为他们的内容创建一个新的块级格式化上下文。 在一个块级格式化上下文中,块级元素会从改块级元素的顶端从上往下一个一个依次布局。两个相邻的块级元素之前的垂直间距由他们的 margin 值决定。两个相邻块级元素的垂直距离会发生叠加。 在一个块级格式化上下文中,每一个元素框的左外边缘与容器包含块左边缘相接触(从右到左的模式就是右边缘接触)即使纯在浮动也是如此(虽然在存在浮动的情况下,元素的框高会塌陷)除非创建一个新的块级格式化上下文(在该情况下元素有可能由于浮动变窄)。

简单的说:

FC: 格式化上下文,页面中独立的一个渲染区域,以及自身的一套渲染规则,决定子元素的定位于相关作用。主要包括BFC和IFC BFC: 块级格式化上下文,独立的块级渲染区域,该区域拥有自身的一套渲染规则来约束块级盒子的布局,与区域外部无关。 IFC: 行内格式化上下文。

块级元素与BFC

是不是会感觉块级元素与BFC是不是有些类似,又有挺大差别。直接说的话,块级元素是一个实实在在的你所能看到的一个个块级的DOM节点,而BFC是较为虚拟的存在,是拥有对某一区域实现渲染的一套规则的虚拟概念。所以你才可以使用我们所说的BFC去解决块级元素或者行内元素的一些样式问题,这正是因为在你对渲染区域(或者说某一DOM节点)创建新的BFC后而改变了该渲染区域的某一些样式规则来完成你所期望的某种样式实现而已。 一般来说可以看作页面所有的元素处于一个BFC之中(除去内部生成的BFC),因为根节点便是一个BFC。

BFC是如何创建的

  • 根节点元素
  • 浮动元素
  • position 为 absolute 或者 fixed 的元素
  • overlay 不为 visible 的元素
  • dispaly 为 inline-block table-cell 或者 table-caption 的元素

BFC的渲染规则

将w3c的定义详细拆解,可以如下定义: - 内部元素会从区域顶部垂直一个接一个放置。 - 元素垂直方向上的间距由margin决定,属于同一BFC下的相邻元素,垂直的margin会发生重叠。 - 每个元素的左外边会与包含它的BFC的左内边接触,忽略是否有浮动的的兄弟节点。 - BFC区域不会与浮动元素发生重叠。 - BFC计算高度时,会计算其内部浮动元素。 - BFC为页面独立区域,内外元素不会相互影响。

BFC使用实例

文字环绕,重叠问题处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
```
<style>
body {
width: 150px;
}
.float {
width: 50px;
height: 100px;
float: left;
background: #A3F173;
}
.main{
height: 200px;
background: #5C9E4C;
}
</style>
<body>
<div class="float"></div>
<div class="main"></div>
</body>
1
2
3

![](http://freefe.cc/blog/wp-content/uploads/2016/06/20160630191811.png) class 为main 的div被浮动元素覆盖,如内部有文字时,将对其环绕展现,区域被浮动元素覆盖。当将其 BFC 化之后,就会展现BFC区域不会与浮动元素发生重叠的特性

1
2
3
4
5
.main{
height: 200px;
background: #5C9E4C;
overlay: hidden;
}
1
2
3

![](http://freefe.cc/blog/wp-content/uploads/2016/06/20160630192105.png) _清除浮动,父级元素高度计算内部浮动元素_

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<style>
.main{
padding: 5px;
width: 200px;
background: #5C9E4C;
}
.float {
width: 50px;
height: 100px;
float: left;
background: #A3F173;
}
</style>
<body>
<div class="main">
<div class="float"></div>
</div>
</body>
1
2
3

![](http://freefe.cc/blog/wp-content/uploads/2016/06/20160630193213.png) 父级元素内部元素由于其浮动属性,所以父级元素的高度并未计算浮动元素的高度,当将其 BFC化之后,则展现BFC计算高度时,会计算其内部浮动元素

1
2
3
4
5
6
.main{
overflow:hidden;
padding: 5px;
width: 200px;
background: #5C9E4C;
}
1
2
3

![](http://freefe.cc/blog/wp-content/uploads/2016/06/20160630193540.png) 不过上面形式带来的问题是,我们需要将父级元素设置某一属性,使其被BFC化,就很可能会有样式上的冲突问题,所以更好的解决方案为:

1
2
3
4
5
6
7
.main:after {
content: " ";
display: block;
height: 0;
clear: both;
visibility: hidden;
}
1
2
3

_margin 重叠问题_

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
<style>
.top{
margin-bottom: 20px;
height: 100px;
width: 200px;
background: #5C9E4C;
}
.bottom {
margin-top: 30px;
height: 100px;
width: 200px;
background: #A3F173;
}
.center{
margin-top: 40px;
height: 50px;
background: #5C9E4C;
}
</style>
<body>
<div class="top"></div>
<div class="bottom">
<div class="center"></div>
</div>
</body>

```

上面div的margin-bottom为20px,下面div的margin-top为30px,其内部的div的margin-top为40px,最后的最后,两个div之前的间距为 - 40px!这便是块级元素margin叠加与塌陷的问题,可以理解为改三个div均属于同一个BFC下的元素,所以垂直的margin会发生重叠和塌陷。当将下面的div进行BFC话之后你会看到: 所以BFC为页面独立区域,内外元素不会相互影响。

总结

个人认为,了解BFC的渲染特性,有助于在我们平时开发时遇到的一些样式问题的解决,布局。将其作为一个知识点进行了解对我们CSS的开发具有比较大的帮助。包括对于清楚浮动,边距布局等控制与了解。