HTTPS协议浅析

首先,我们在讲HTTPS之前先了解下为什么摒弃HTTP。

HTTP

明文传输协议,交互过程以及数据传输都没有进行加密,通信双方也没有进行任何认证,通信过程非常容易遭遇劫持、监听、篡改,严重情况下,会造成恶意的流量劫持等问题,甚至造成个人隐私泄露(比如银行卡卡号和密码泄露)等严重的安全问题。

HTTPS

超文本安全传输协议,和HTTP相比,多了一个SSL/TSL的认证过程,端口为443。
HTTP协议直接放置在TCP协议之上,而HTTPS在HTTP和TCP中间加上一层加密层。如图2-1所示。


HTTPS
图2-1

HTTPS流程解析

首先,我们来看一下HTTPS的简要流程图(如图3-1)。


HTTPS
图3-1

1.客户端先向服务端发送一次HTTPS的连接请求。HTTPS服务端返回CA证书和加密公钥public key(非对称加密,如RSA)。
2.客户端用预设的CA列表验证证书,若证书有问题则提示风险。
3.客户端生成随机对称密钥,并通过第一步骤中的public key做加密(如AES对称加密)
4.将加密后的对称密钥发送给HTTPS服务端
5.HTTPS服务端用自己的非对称密钥private key解密,得到第三步的随机对称密钥
6.之后双方传送数据都用第五步得到的对称密钥加密后通信

注意点:

  1. 第一步中为什么使用非对称加密(如RSA)?
    因为非对称加密中,公钥加密的数据,有且只有唯一的私钥才能够解密。栗子:我有N把锁但只对应一把钥匙,这把钥匙保存在HTTPS服务器中,锁通过第一步的请求分发给各个客户端。
  1. 第三步中为什么使用对称加密(如AES)?
    因为非对称加密的解密过程极其消耗CPU资源,如果在通信过程中均使用非对称加密,会对服务器产生巨大的压力。因此非对称加密只是在对称密钥交换或者CA签名的时候使用(第一步和第五步)。之后一直使用对称加密进行通信,因为对称加密的效率高、速度快。
    栗子:对称加密就是一把锁对应一把钥匙,第四步就是客户端把自己的钥放进过public key这把锁里,然后发给HTTPS服务端,服务端用自己的private key打开这把锁,得到了里面的对称密钥。这样客户端和服务端双方手里都拿着对称密钥,之后就可以拿着这把锁互相加解密了。
  1. 为什么需要CA数字证书?
    数字证书有三个功能:
    1.身份授权。确保浏览器访问的网站是经过CA验证的可信任的网站;
    2.分发公钥。每个数字证书都包含了注册者生成的公钥(验证确保是合法的,非伪造的公钥)。在SSL握手时会通过certificate消息传输给客户端;
    3.验证证书合法性。客户端接收到数字证书后,会对证书合法性进行验证。只有验证通过后的证书,才能够进行后续通信过程(通过数字签名防伪SHA-RSA)。
    如果没有数字证书,第一步中的非对称密钥可能会被中间人拦截,然后自己创建一个新的非对称密钥给客户端,从而导致客户端和服务端的通信均可被中间人解密了

综上所述,如图3-2,非对称加密算法(公钥和私钥)交换对称密钥+数字证书验证身份(验证公钥是否是伪造的)+利用对称密钥加解密后续传输的数据=安全


HTTPS
图3-2

HTTPS基本的流程如上所示,其中的非对称加密、对称加密和数字签名可以展开很多篇幅,这里就不详细展开了。可以通过对应用密码学的学习,对HTTPS的加解密过程进行更深入的理解。

Flux与Redux浅析

什么是Flux?

—— 简单地说,Flux是用来构建用户端的Web应用程序的体系架构,用来管理和控制应用中数据的流向。

Flux数据流

从上图可知,Flux最大的一个优点就是数据的单向流动。

1.首先要有 action,通过定义一些 action creator 方法根据需要创建 Action 提供给 dispatcher
2.View 层通过用户交互(比如 onClick)会触发 Action
3.Dispatcher 会分发触发的 Action 给所有注册的 Store 的回调函数
4.Store 回调函数根据接收的 Action 更新自身数据之后会触发一个 change 事件通知 View 数据更改了
5.View 会监听这个 change 事件,拿到对应的新数据并调用 setState 更新组件 UI

因此,我们可以将上图总结一下:
Action可以看成是修改Store的行为抽象;
Dispatcher管理着应用的数据流,可以看为Action到Store的分发器;
Store管理着整个应用的状态和逻辑,类似MVC中的Model。

相对于MVC中View具备多个修改Modal的能力,导致VC<->M在复杂页面中会比较混乱。
MVC

Flux实现了所有的状态都通过Store来维护,Store统一控制View,View没有直接修改Store的能力,而是发起Action通过Dispatcher去修改Store(通过 Action 传递数据)。避免了数据流的混乱。

Flux Demo

什么是Redux?

—— Redux是基于Flux架构的一次改进,官方的定义:Redux is a predictable state container for JavaScript apps(可预测的状态容器)。

Redux数据流

从图中可以看出,Redux和Flux最大的不同在于Redux用Reducer代替了Flux的Dispatcher。Redux设想你永远不会变动你的数据,而应该在Reducer中返回新的对象来作为应用的新状态。因此,State 应该是只读的,唯一改变 State 的方法就是触发Action。而为了描述 Action 如何改变 State Tree ,需要编写 Reducers来实现。

Reducer函数:是纯函数,不应该有副作用,不应有API调用,Date.now()或者随机获取等不稳定的操作,应当保证相同的输入Reducer计算的结果应该是一致的输出,它只会进行单纯的计算。

1
2
3
4
5
6
7
8
9
function Reducer(state, action) {
switch (action.type) {
case ACTION_TYPE:
// calc...
return newState;
default: return state;
}
return newState;
}

State是不可修改的,所以返回的新State应该是基于输入State副本的修改,而不是直接修改State后的返回。

Redux三大原则

1.单一数据源(Store)
整个应用的State被存放在一棵Object tree中,并且这个Object tree只存在唯一一个Store中;
2.State是只读的 唯一改变 State 的方法就是触发 Action,Action 是一个用于描述已发生事件的普通对象。
确保了View和网络请求无法直接修改State,所有的修改都能被集中化处理。
3.通过纯函数Reducer来修改Store,
Reducer 只是一些纯函数,它接收先前的 State 和 Action,并返回新的 State。
即reducer(state, action) => new state

Redux与Flux的对比

前面我们称Redux是基于Flux架构的一次改进,那么Redux和Flux具体有哪些区别呢?从代码层面来看:
Action:

  • Flux:

    1
    2
    3
    4
    5
    6
    7
    /*Flux直接在Action中调用Dispatch*/
    export function addTodo(text) {
    AppDispatcher.dispatch({
    type: ActionTypes.ADD_TODO,
    text: text
    });
    }
  • Redux:

    1
    2
    3
    4
    5
    6
    7
    /*Redux将Action和Dispatch解耦*/
    export function addTodo(text) {
    return {
    type: ActionTypes.ADD_TODO,
    text: text
    };
    }

Store:

  • Flux:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    let _todos = [];
    const TodoStore = Object.assign(new EventEmitter(), {
    getTodos() {
    return _todos;
    }
    });
    AppDispatcher.register(function (action) {
    switch (action.type) {
    case ActionTypes.ADD_TODO:
    _todos = _todos.concat([action.text]);
    TodoStore.emitChange();
    break;
    }
    });
    export default TodoStore;
  • Redux:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    /*Redux将Dispatch从Store中剥离*/
    const initialState = { todos: [] };
    export default function TodoStore(state = initialState, action) {
    switch (action.type) {
    case ActionTypes.ADD_TODO:
    return { todos: state.todos.concat([action.text]) };
    default:
    return state;
    }

Redux简单demo实现

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
// actions
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';

function incrementCreator(number) {
return {
type: INCREMENT,
number,
};
}

function decrementCreator(number) {
return {
type: DECREMENT,
number,
};
}

// 初始化state
const initialState = {
counter: 0,
};

// reducers函数,注意最后一定要return state防止不能匹配到action的时候state丢失
function countReducer(state = initialState, action) {
switch (action.type) {
case INCREMENT:
return Object.assign({}, {
counter: state.counter + action.number,
});
case DECREMENT:
return Object.assign({}, {
counter: state.counter - action.number,
});
default: return state;
}
}

// 创建store
const store = Redux.createStore(countReducer);

// 订阅store的修改
store.subscribe(function log() {
console.log(store.getState());
});

// 通过dispatch action来改变state
store.dispatch(incrementCreator(5)); //Object {counter: 5}
store.dispatch(decrementCreator(4)); //Object {counter: 1}

JavaScript并发模型与Event Loop

并发模型可视化描述

model.svg

如上图所示,Javascript执行引擎的主线程运行的时候,产生堆(heap)和栈(stack),程序中代码依次进入栈中等待执行,若执行时遇到异步方法,该异步方法会被添加到用于回调的队列(queue)中【即JavaScript执行引擎的主线程拥有一个执行栈/堆和一个任务队列】。

栈(stack) : 函数调用会形成了一个堆栈帧
堆(heap) : 对象被分配在一个堆中,一个用以表示一个内存中大的未被组织的区域。
队列(queue) : 一个 JavaScript 运行时包含了一个待处理的消息队列。每一个消息都与一个函数相关联。当栈为空时,则从队列中取出一个消息进行处理。这个处理过程包含了调用与这个消息相关联的函数(以及因而创建了一个初始堆栈帧)。当栈再次为空的时候,也就意味着该消息处理结束。

为了更清晰地描述Event Loop,参考下图的描述:

model.png

首先,我们对图中的一些名词稍加解释

  1. queue : 如上文的解释,值得注意的是,除了IO设备的事件(如load)会被添加到queue中,用户操作产生 的事件(如click,touchmove)同样也会被添加到queue中。队列中的这些事件会在主线程的执行栈被清空时被依次读取(队列先进先出,即先被压入队列中的事件会被先执行)。
  2. callback : 被主线程挂起来的代码,等主线程执行队列中的事件时,事件对应的callback代码就会被执行

【注:因为主线程从”任务队列”中读取事件的过程是循环不断的,因此这种运行机制又称为Event Loop(事件循环)】

下面我们通过setTimeout来看看单线程的JavaScript执行引擎是如何来执行该方法的。

  1. JavaScript执行引擎主线程运行,产生heap和stack
  2. 从上往下执行同步代码,log(1)被压入执行栈,因为log是webkit内核支持的普通方法而非WebAPIs的方法,因此立即出栈被引擎执行,输出1
  3. JavaScript执行引擎继续往下,遇到setTimeout()t异步方法(如图,setTimeout属于WebAPIs),将setTimeout(callback,5000)添加到执行栈
  4. 因为setTimeout()属于WebAPIs中的方法,JavaScript执行引擎在将setTimeout()出栈执行时,注册setTimeout()延时方法交由浏览器内核其他模块(以webkit为例,是webcore模块)处理
  5. 继续运行setTimeout()下面的log(3)代码,原理同步骤2
  6. 当延时方法到达触发条件,即到达设置的延时时间时(5秒后),该延时方法就会被添加至任务队列里。这一过程由浏览器内核其他模块处理,与执行引擎主线程独立
  7. JavaScript执行引擎在主线程方法执行完毕,到达空闲状态时,会从任务队列中顺序获取任务来执行。
  8. 将队列的第一个回调函数重新压入执行栈,执行回调函数中的代码log(2),原理同步骤2,回调函数的代码执行完毕,清空执行栈
  9. JavaScript执行引擎继续轮循队列,直到队列为空
  10. 执行完毕
1
2
3
4
5
6
7
8
9
10
console.log(1);
setTimeout(function() {
console.log(2);
},5000);
console.log(3);

//输出结果:
//1
//3
//2

Macrotasks 和 Microtasks

基本上,一个完整的事件循环模型就讲完了。现在我们来重点关注一下队列。
异步任务分为两种:Macrotasks 和 Microtasks。

  • Macrotasks: setTimeout, setInterval, setImmediate, I/O, UI rendering
  • Microtasks: process.nextTick, Promises, Object.observe(废弃), MutationObserver

Macrotasks 和 Microtasks有什么区别呢?我们以setTimeout和Promises来举例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
console.log('1');
setTimeout(function() {
console.log('2');
}, 0);
Promise.resolve().then(function() {
console.log('3');
}).then(function() {
console.log('4');
});
console.log('5');
//输出结果:
//1
//5
//3
//4
//2

原因是Promise中的then方法的函数会被推入 microtasks 队列,而setTimeout的任务会被推入 macrotasks 队列。在每一次事件循环中,macrotask 只会提取一个执行,而 microtask 会一直提取,直到 microtasks 队列清空。
结论如下:

  1. microtask会优先macrotask执行
  2. microtasks会被循环提取到执行引擎主线程的执行栈,直到microtasks任务队列清空,才会执行macrotask

【注:一般情况下,macrotask queues 我们会直接称为 task queues,只有 microtask queues 才会特别指明。】

【参考链接】

JavaScript 运行机制详解:再谈Event Loop
并发模型与Event Loop
【转向Javascript系列】从setTimeout说事件循环模型
异步 JavaScript 之理解 macrotask 和 microtask

自定义浏览器返回逻辑

业务场景:当某一页面有重要信息,返回操作可能会导致信息丢失或无法再回到该页面。如:用户随机抽奖,获取某实物奖品需要用户填写地址来寄送奖品,若用户返回则无法获取到获奖用户地址信息且无法再次回到填写地址的页面。

因此,类似于填写地址的页面,通常会有返回二次确认的逻辑。在APP里实现很简单,只需要订阅appbridge实例的自定义返回事件即可。但在其他APP内置浏览器和普通浏览器返回就不能拦截了。

这种情况下我们通过 history.pushState() + popstate event 的方法来实现自定义浏览器返回的需求。

关于 history.pushState

通过history.pushState() 方法可以向浏览器历史添加了一个状态。

语法 history.pushState(state, title, url); [state:状态对象,title:标题,url:新历史纪录的地址]

1
2
3
4
5
6
//向history中添加一条状态
var state = { 'page_id': 1, 'user_id': 5 };
var title = 'Hello World';
var url = 'hello-world.html';

history.pushState(state, title, url);

浏览器兼容良好,兼容情况可查看下方的 “MDN: History.pushState()详解”

MDN: History.pushState()详解

关于 popstate event

浏览器的前进后退行为(或者在JavaScript中调用history.back()、history.forward()、history.go()方法)会触发popstate事件。【注:history.pushState()或者history.replaceState()不会触发popstate事件】

语法 window.onpopstate = function() {};

1
2
3
4
5
window.onpopstate = function(event) {
console.log(JSON.stringify(event.state));
};

history.pushState({'page_id': 1}, "title 1", "?page=1"); //添加并激活一个历史记录条目

MDN: window.onpopstate详解

通过结合history.pushState()和popstate event实现自定义返回的例子

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
//向history中添加一条状态
window.onload = function() {
//调用history.pushState()方法,向history对象中添加一条状态
history.pushState({
'page_id': 1,
},'after','/after.html');
};

window.onpopstate = function(e) {
//Modal让用户选择是确定返回还是继续留在本页面
var modal = document.createElement('div');
modal.innerHTML = '<div class="content" >\
<p>文案</p>\
</div>\
<div class="btn-box">\
<a id="btn-cancel"class="btn btn-cancel" href="javascript: void(0)">取消</a>\
<a id="btn-confirm" class="btn btn-confirm" href="javascript: void(0)">确定</a>\
</div>';
document.body.appendChild(modal);
var cancelBtn = document.getElementById('btn-cancel');
var confirmBtn = document.getElementById('btn-confirm');

cancelBtn.onclick = function() {
history.pushState({
'page_id': 1,
},'after','/after.html');
document.body.removeChild(modal);
};
confirmBtn.onclick = function() {
history.go(-1);
document.body.removeChild(modal);
};
};

点击查看实现效果http://5.zhangjinxin.applinzi.com/before.html

JavaScript设计模式

设计模式的定义:在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案

当然我们可以用一个通俗的说法:设计模式是解决某个特定场景下对某种问题的解决方案。因此,当我们遇到合适的场景时,我们可能会条件反射一样自然而然想到符合这种场景的设计模式。

比如,当系统中某个接口的结构已经无法满足我们现在的业务需求,但又不能改动这个接口,因为可能原来的系统很多功能都依赖于这个接口,改动接口会牵扯到太多文件。因此应对这种场景,我们可以很快地想到可以用适配器模式来解决这个问题。

下面介绍几种在JavaScript中常见的几种设计模式:

##1.单例模式

单例模式的定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。实现的方法为先判断实例存在与否,如果存在则直接返回,如果不存在就创建了再返回,这就确保了一个类只有一个实例对象。

适用场景:一个单一对象。比如:弹窗,无论点击多少次,弹窗只应该被创建一次。

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
class CreateUser {
constructor(name) {
this.name = name;
this.getName();
}

getName() {
return this.name;
}
}

// 代理实现单例模式
var ProxyMode = (function() {
var instance = null;
return function(name) {
if(!instance) {
instance = new CreateUser(name);
}
return instance;
}
})();
// 测试单体模式的实例
var a = new ProxyMode("aaa");
var b = new ProxyMode("bbb");

// 因为单体模式是只实例化一次,所以下面的实例是相等的
console.log(a === b); //true