-
🔭 I’m working at Alibaba
-
🌱 I’m currently learning golang/java/spring cloud/hadoop/spark
39er / blog_abandoned Goto Github PK
View Code? Open in Web Editor NEW一些开发拙见
一些开发拙见
在函数的执行过程中,如果最后一个动作是一个函数的调用,即这个调用的返回值被当前函数直接返回,则称为尾调用
如:
function a(x) {
return b(x);
}
而如下则不是尾调用:
function f(x) {
return 1 + g(x)
}
原因是它的最后一步操作是将 g 函数调用的返回值和 1 进行加法操作,而不是调用其他函数,所以它不是尾调用。
尾调用的重要性:它不会在调用栈上增加新的堆栈帧,而是直接更新调用栈,调用栈所占空间始终是常量,节省了内存,避免了爆栈的可能性。
函数调用会在内存形成一个"调用记录",又称"调用帧"(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用记录上方,还会形成一个B的调用记录。等到B运行结束,将结果返回到A,B的调用记录才会消失。如果函数B内部还调用函数C,那就还有一个C的调用记录栈,以此类推。所有的调用记录,就形成一个"调用栈"(call stack)。
尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用记录,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用记录,取代外层函数的调用记录就可以了。
顾名思义,在一个尾调用中,如果函数最后的尾调用位置上是这个函数本身,则被称为尾递
归。递归很常用,但如果没写好的话也会非常消耗内存,导致爆栈。但是对于尾递归来说只
存在一个调用栈,便永远不会发生“栈溢出”错误。
就以求一个给出数的阶乘来探索吧。在传统的做法中利用n的递减乘上原函数,这样复杂度
便会很高,数据量一大便会发生“栈溢出”的错误了。
传统的解决办法
function f(n) {
if(n === 1) {
return 1;
}
return n * f(n -1);
}
尾调用
function a(n, t) {
if(n === 1) {
return t;
}
return a(n - 1, n * t)
}
对比两个解决办法的复杂度就知道,尾调用只保留了一个记录。
上面的尾递归方法虽然可以避免发生“栈溢出”的错误,但只是一个普通的阶乘方法,传递两个参数,第二个参数不容易让调用者理解,可读性差,优化方式有两种
柯里化:将多参数函数转换成单参数函数
function currying(fn,n){
return function(m){
fn.call(this,m,n);
}
}
const b = currying(a,1);
b(n);
2、使用ES6初始化参数默认值特性
function a(n,t=1){
if(n === 1) {
return t;
}
return a(n - 1, n * t)
}
参考文章:阮老师的这篇文章
还有:https://www.keephhh.com/2017/05/02/tc/
Zookeeper是一个分布式的、开源的分布式应用协调服务。它暴露了一组简单的基础原件,分布式应用可以在这些原件之上实现更高级别的服务,如同步、配置维护、群组、和命名。它的设计非常容易编程实现,并且使用一个常见的文件系统的树型结构的数据模型。它运行在Java中,并且绑定了Java和C。
基于观察者模式设计的分布式服务管理框架
负责存储和管理大家都关心的数据,然后接受观察者的注册,一旦这些数据的状态发生变化,Zookeeper 就将负责通知已经在 Zookeeper 上注册的那些观察者做出相应的反应,从而实现集群中类似 Master/Slave 管理模式
主要是用来解决分布式应用中经常遇到的一些数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等。
ZooKeeper 有一个类似于文件系统的数据模型,由 znodes 组成。可以将 znodes(ZooKeeper 数据节点)视为类似 UNIX 的传统系统中的文件,但它们可以有子节点。另一种方式是将它们视为目录,它们可以有与其相关的数据。每个这些目录都被称为一个 znode。
znode 层次结构被存储在每个 ZooKeeper 服务器的内存中。这实现了对来自客户端的读取操作的可扩展的快速响应。每个 ZooKeeper 服务器还在磁盘上维护了一个事务日志,记录所有的写入请求。因为 ZooKeeper 服务器在返回一个成功的响应之前必须将事务同步到磁盘,所以事务日志也是 ZooKeeper 中对性能最重要的组成部分。可以存储在 znode 中的数据的默认最大大小为 1 MB。因此,即使 ZooKeeper 的层次结构看起来与文件系统相似,也不应该将它用作一个通用的文件系统。相反,应该只将它用作少量数据的存储机制,以便为分布式应用程序提供可靠性、可用性和协调。
Zookeeper 这种数据结构有如下这些特点:
再次强调:Zookeeper 并不是用来专门存储数据的,它的作用主要是用来维护和监控你存储的数据的状态变化。
Zookeeper非常简单和高效。因为它的目标就是作为建设复杂服务的基础,比如同步。zookeeper提供了一套保证,他们包括:
方法名 | 方法描述 |
---|---|
Stringcreate(String path, byte[] data, List acl, CreateMode createMode) | 创建一个给定的目录节点 path, 并给它设置数据,CreateMode 标识有四种形式的目录节点,分别是 PERSISTENT:持久化目录节点,这个目录节点存储的数据不会丢失;PERSISTENT_SEQUENTIAL:顺序自动编号的目录节点,这种目录节点会根据当前已近存在的节点数自动加 1,然后返回给客户端已经成功创建的目录节点名;EPHEMERAL:临时目录节点,一旦创建这个节点的客户端与服务器端口也就是 session 超时,这种节点会被自动删除;EPHEMERAL_SEQUENTIAL:临时自动编号节点 |
Statexists(String path, boolean watch) | 判断某个 path 是否存在,并设置是否监控这个目录节点,这里的 watcher 是在创建 ZooKeeper 实例时指定的 watcher,exists方法还有一个重载方法,可以指定特定的 watcher |
Statexists(String path, Watcher watcher) | 重载方法,这里给某个目录节点设置特定的 watcher,Watcher 在 ZooKeeper 是一个核心功能,Watcher 可以监控目录节点的数据变化以及子目录的变化,一旦这些状态发生变化,服务器就会通知所有设置在这个目录节点上的 Watcher,从而每个客户端都很快知道它所关注的目录节点的状态发生变化,而做出相应的反应 |
void delete(String path, int version) | 删除 path 对应的目录节点,version 为 -1 可以匹配任何版本,也就删除了这个目录节点所有数据 |
List getChildren(String path, boolean watch) | 获取指定 path 下的所有子目录节点,同样 getChildren方法也有一个重载方法可以设置特定的 watcher 监控子节点的状态 |
StatsetData(String path, byte[] data, int version) | 给 path 设置数据,可以指定这个数据的版本号,如果 version 为 -1 怎可以匹配任何版本 |
byte[] getData(String path, boolean watch, Stat stat) | 获取这个 path 对应的目录节点存储的数据,数据的版本等信息可以通过 stat 来指定,同时还可以设置是否监控这个目录节点数据的状态 |
void addAuthInfo(String scheme, byte[] auth) | 客户端将自己的授权信息提交给服务器,服务器将根据这个授权信息验证客户端的访问权限。 |
Stringcreate(String path, byte[] data, List acl, CreateMode createMode) | 创建一个给定的目录节点 path, 并给它设置数据,CreateMode 标识有四种形式的目录节点,分别是 PERSISTENT:持久化目录节点,这个目录节点存储的数据不会丢失;PERSISTENT_SEQUENTIAL:顺序自动编号的目录节点,这种目录节点会根据当前已近存在的节点数自动加 1,然后返回给客户端已经成功创建的目录节点名;EPHEMERAL:临时目录节点,一旦创建这个节点的客户端与服务器端口也就是 session 超时,这种节点会被自动删除;EPHEMERAL_SEQUENTIAL:临时自动编号节点 |
Statexists(String path, boolean watch) | 判断某个 path 是否存在,并设置是否监控这个目录节点,这里的 watcher 是在创建 ZooKeeper 实例时指定的 watcher,exists方法还有一个重载方法,可以指定特定的 watcher |
Statexists(String path, Watcher watcher) | 重载方法,这里给某个目录节点设置特定的 watcher,Watcher 在 ZooKeeper 是一个核心功能,Watcher 可以监控目录节点的数据变化以及子目录的变化,一旦这些状态发生变化,服务器就会通知所有设置在这个目录节点上的 Watcher,从而每个客户端都很快知道它所关注的目录节点的状态发生变化,而做出相应的反应 |
void delete(String path, int version) | 删除 path 对应的目录节点,version 为 -1 可以匹配任何版本,也就删除了这个目录节点所有数据 |
List getChildren(String path, boolean watch) | 获取指定 path 下的所有子目录节点,同样 getChildren方法也有一个重载方法可以设置特定的 watcher 监控子节点的状态 |
StatsetData(String path, byte[] data, int version) | 给 path 设置数据,可以指定这个数据的版本号,如果 version 为 -1 怎可以匹配任何版本 |
byte[] getData(String path, boolean watch, Stat stat) | 获取这个 path 对应的目录节点存储的数据,数据的版本等信息可以通过 stat 来指定,同时还可以设置是否监控这个目录节点数据的状态 |
void addAuthInfo(String scheme, byte[] auth) | 客户端将自己的授权信息提交给服务器,服务器将根据这个授权信息验证客户端的访问权限。 |
StatsetACL(String path, List acl, int version) | 给某个目录节点重新设置访问权限,需要注意的是 Zookeeper 中的目录节点权限不具有传递性,父目录节点的权限不能传递给子目录节点。目录节点 ACL 由两部分组成:perms 和 id。Perms 有 ALL、READ、WRITE、CREATE、DELETE、ADMIN 几种而 id 标识了访问目录节点的身份列表,默认情况下有以下两种:ANYONE_ID_UNSAFE = new Id("world", "anyone") 和 AUTH_IDS = new Id("auth", "") 分别表示任何人都可以访问和创建者拥有访问权限。 |
List getACL(String path, Stat stat) | 获取某个目录节点的访问权限列表 |
tickTime=2000
dataDir=D:/devtools/zookeeper-3.2.2/build
clientPort=2181
当这些配置项配置好后,你现在就可以启动 Zookeeper 了,启动后要检查 Zookeeper 是否已经在服务,可以通过 netstat – ano 命令查看是否有你配置的 clientPort 端口号在监听服务。
initLimit=5
syncLimit=2
server.1=192.168.211.1:2888:3888
server.2=192.168.211.2:2888:3888
除了修改 zoo.cfg 配置文件,集群模式下还要配置一个文件 myid,这个文件在 dataDir 目录下,这个文件里面就有一个数据就是 A 的值,Zookeeper 启动时会读取这个文件,拿到里面的数据与 zoo.cfg 里面的配置信息比较从而判断到底是那个 server。
参考资料:
function Super() {}
function Sub() {}
Sub.prototype = new Super();
Sub.prototype.constructor = Sub;
var sub = new Sub();
Sub.prototype.constructor === Sub; // ② true
sub.constructor === Sub; // ④ true
sub.__proto__ === Sub.prototype; // ⑤ true
Sub.prototype.__proto__ == Super.prototype; // ⑦ true
class Super {}
class Sub extends Super {}
var sub = new Sub();
Sub.prototype.constructor === Sub; // ② true
sub.constructor === Sub; // ④ true
sub.__proto__ === Sub.prototype; // ⑤ true
Sub.__proto__ === Super; // ⑥ true
Sub.prototype.__proto__ === Super.prototype; // ⑦ true
ES6和ES5的继承是一模一样的,只是多了class 和extends ,ES6的子类和父类,子类原型和父类原型,通过__proto__ 连接。
原文链接:ES5和ES6中的继承
看到这张图忽然引发了自己的困惑,自己的定位到底是什么呢?前端Or后端?Node.js的定位又算是什么呢?搞了两年多java和node.js,也一直在写着一些前端页面,对热门前端框架(react、vue、angular)也学了不少,
感觉自己现在的定位十分尴尬...难道要发展成传说中的“全沾工程师”了吗...
另对现在主要使用的node.js的前景不是很明了,node.js到底算是前端还是后端呢?问出这句话肯定很多人会喷node.js肯定是服务器语言呀,不知道还说自己是搞node的,等等....但是,仔细想想,node在后端的大规模应用感觉还是不是很现实,因为node自身现在还是极不成熟的,js也存在其语言本身的一些弊病,正如偶像TJ大神都去搞Go了一样。。。感觉真正的后端工程师也是不屑于使用这门性能不高的语言的...反而适合于前端工程师们快速去适应一门服务器语言,可以提供一些快速的webserver。个人感觉nodejs十分适合于作为api的提供方,去调用其他后端处理,反馈给前端页面。所以感觉node.js没有核心地位,不像java、Ruby、Python那么稳...
以上纯属个人头脑发热,欢迎指点...
yarn.lock锁定了依赖包的版本,保证其它机子也安装相同版本的包,同时包含了 package.json 中定义的一系列允许的版本。
yarn install 的时候会先使用缓存,所以再次下载速度会比npm快一些
yarn采用并行安装的方式
yarn & npm 常用命令比较
npm install === yarn / yarn install
npm install xxx —save === yarn add xxx
npm uninstall xxx —save === yarn remove xxx
npm install xxx —save-dev === yarn add xxx —dev
npm update === yarn upgrade
npm install xxx -g === yarn global add xxx
但个人用过一段时间yarn之后有了这样的反思:
在使用yarn时发现,但项目所依赖的包a如果依赖其他包b(semver '~')时,yarn会锁定这个b的版本,而当b包有小版本更迭时,直接使用yarn upgrade a 依旧不会更新 b的版本,这点就像npm update a 一样。这时如果删除node_modules,重新yarn intall 时,就会出现问题,因为yarn.lock已经锁定了b的版本,所以再次install依旧安装的是老版本的,也就是说这样下去可能只有等到a包的下一个版本更迭才能更新到b包的新版本,所以这时需要将yarn.lock也一并删除,或 yarn remove a,重新 install a,但这样与npm 无异,且一不小心可能会将b的老版本一直锁在yarn.lock中。在使用antd的时候就发现了这个问题,其中依赖rc-select包,当rc-select更新时,只有re-install antd 才能更新rc-select。
另外看了死马的这篇文章后加深了这种反思。也许在今后更多的使用中我会有新的发现和感想。
会将一些我认为经典的代码片段放在这里,没事拿出来学习一下,像TJ大神一下多看别人的代码,想一想如果是我来做会怎样来实现。
(由于截图都会放在评论里,所以本文禁止评论...)
Stream 是Node.js 的一个基础模块,继承了EventEmitter,是很多模块的基础,几乎所有的I/O操作都与其有关。
Stream 的分类包括Readable、Writable、Duplex、Transform
通过 Stream 源码 可了解具体分类:
const Stream = module.exports = require('internal/streams/legacy');
Stream.Readable = require('_stream_readable');
Stream.Writable = require('_stream_writable');
Stream.Duplex = require('_stream_duplex');
Stream.Transform = require('_stream_transform');
Stream.PassThrough = require('_stream_passthrough');
// Backwards-compat with node 0.4.x
Stream.Stream = Stream;
常见的Readable 如下:
new stream.Readable([options])
options <Object>
highWaterMark <number> :The maximum number of bytes to store in the internal buffer before ceasing to read from the underlying resource. Defaults to 16384 (16kb), or 16 for objectMode streams
encoding <string>: If specified, then buffers will be decoded to strings using the specified encoding. Defaults to null
objectMode <boolean>: Whether this stream should behave as a stream of objects. Meaning that stream.read(n) returns a single value instead of a Buffer of size n. Defaults to false
read <Function> Implementation for the stream._read() method.
destroy Implementation for the stream._destroy() method.
Readable stream有两种模式:
可以通过以下几种方法切换到following模式:
可以通过以下几种方法切换到paused模式:
paused 例子:
var Readable = require('stream').Readable;
var rs = new Readable();
rs.on('readable', function() {
var chunk = rs.read();
console.log('get data:', chunk ? chunk.toString() : null);
});
rs.on('end', function() {
console.log('stream end');
});
rs.push('hello stream');
rs.push('hello alex');
rs.push(null);
flowing 例子:
var Readable = require('stream').Readable;
var rs = new Readable();
rs.on('data', function(chunk) {
console.log('get data:', chunk.toString());
});
rs.on('end', function() {
console.log('stream end');
});
rs.push('hello stream');
rs.push('hello alex');
rs.push(null);
在实际情况下,如果要实现一个自定义的Readable stream类,往往是通过定义其_read方法来进行数据的处理。看如下例子:
var Readable = require('stream').Readable;
function MyReadable(data, options) {
if (!(this instanceof MyReadable)) {
return new MyReadable(data, options);
}
Readable.call(this, options);
this.data = data || [];
this.index = 0;
}
MyReadable.prototype.__proto__ = Readable.prototype;
MyReadable.prototype._read = function() {
if (this.index >= this.data.length) {
this.push(null);
} else {
setTimeout(function() {
this.push(this.data[this.index++]);
}.bind(this), 1000);
}
};
var data = ['California Dreaming', 'Hotel California', 'Californication'];
var rs = MyReadable(data);
rs.on('data', function(chunk) {
console.log('get data:', chunk.toString());
});
实现一个Readable,需要实现_read()方法:
const { Readable } = require('stream');
class MyReadable extends Readable {
constructor(options) {
// Calls the stream.Readable(options) constructor
super(options);
// ...
}
_read(size){
// ...
}
}
通过readable.push(chunk[, encoding])方法来向缓冲区中写入数据
常见的如下几种:
Constructor: new stream.Writable([options])
options <Object>
highWaterMark <number> Buffer level when stream.write() starts returning false. Defaults to 16384 (16kb), or 16 for objectMode streams.
decodeStrings <boolean> Whether or not to decode strings into Buffers before passing them to stream._write(). Defaults to true
objectMode <boolean> Whether or not the stream.write(anyObj) is a valid operation. When set, it becomes possible to write JavaScript values other than string, Buffer or Uint8Array if supported by the stream implementation. Defaults to false
write <Function> Implementation for the stream._write() method.
writev <Function> Implementation for the stream._writev() method.
destroy <Function> Implementation for the stream._destroy() method.
final <Function> Implementation for the stream._final() method.
当创建一个Writable stream的时候,我们需要实现其_write()方法。看下面例子:
var Writable = require('stream').Writable;
var ws = Writable();
ws._write = function(chunk, encoding, cb) {
console.log(chunk.toString());
cb();
}
ws.on('finish', function() {
console.log('on finish');
});
ws.write('hello world');
ws.write('hello alex');
ws.end();
实现Writable:
const { Writable } = require('stream');
class MyWritable extends Writable {
constructor(options) {
// Calls the stream.Writable() constructor
super(options);
// ...
}
}
Duplex 同时实现了 Readable 和 Writable。
new stream.Duplex(options)
options <Object> Passed to both Writable and Readable constructors. Also has the following fields:
allowHalfOpen <boolean> Defaults to true. If set to false, then the stream will automatically end the readable side when the writable side ends and vice versa.
readableObjectMode <boolean> Defaults to false. Sets objectMode for readable side of the stream. Has no effect if objectMode is true.
writableObjectMode <boolean> Defaults to false. Sets objectMode for writable side of the stream. Has no effect if objectMode is true.
常见的Duplex stream有:
如果要实现一个Duplex stream,需要实现它的_read()和_write()方法。
如下例子,实现了一个比较简单的双向流:
var util = require('util'),
stream = require('stream'),
Readable = stream.Readable,
Duplex = stream.Duplex;
function MyReadable(options) {
if (!(this instanceof MyReadable)) {
return new MyReadable(options);
}
Readable.call(this, options);
this._cur = 1;
this._max = 20;
}
util.inherits(MyReadable, Readable);
MyReadable.prototype._read = function() {
if (this._cur > this._max) {
this.push(null);
} else {
this.push('' + this._cur++);
}
}
function MyDuplex(options) {
if (!(this instanceof MyDuplex)) {
return new MyDuplex(options);
}
Duplex.call(this, options);
this._data = [];
}
util.inherits(MyDuplex, Duplex);
MyDuplex.prototype._read = function() {
if (this._data.length) {
this.push(this._data.shift());
this.push('\n');
} else {
this.push(null);
}
}
MyDuplex.prototype._write = function(chunk, encoding, cb) {
console.log('write data:', chunk.toString());
this._data.push(chunk);
cb();
};
var rs = MyReadable(),
ds = MyDuplex();
rs.pipe(ds).pipe(process.stdout);
Transform是一种特殊的Duplex stream,它可以对数据进行转换,也就是说,它的输出是将输入根据某种规则计算而成的。常见的Transform stream有:
new stream.Transform([options])
options <Object> Passed to both Writable and Readable constructors. Also has the following fields:
transform <Function> Implementation for the stream._transform() method.
flush <Function> Implementation for the stream._flush() method.
如果要实现一个Transform stream,需要实现它的_transform()方法。
如下例子,实现了一个比较简单的转换流:
var util = require('util'),
stream = require('stream'),
Readable = stream.Readable,
Transform = stream.Transform;
function MyReadable(options) {
if (!(this instanceof MyReadable)) {
return new MyReadable(options);
}
Readable.call(this, options);
this._cur = 1;
this._max = 20;
}
util.inherits(MyReadable, Readable);
MyReadable.prototype._read = function() {
if (this._cur > this._max) {
this.push(null);
} else {
this.push('' + this._cur++);
}
}
function MyTransform(options) {
if (!(this instanceof MyTransform)) {
return new MyTransform(options);
}
Transform.call(this, options);
}
util.inherits(MyTransform, Transform);
MyTransform.prototype._transform = function(chunk, encoding, cb) {
var val = Number(chunk.toString());
this.push('' + val * 2 + '\n');
cb();
};
var rs = MyReadable(),
ts = MyTransform();
rs.pipe(ts).pipe(process.stdout);
Duplex 虽然同事具备可读流和可写流,但两者是相对独立的;Transform 的可读流的数据会经过一定的处理过程自动进入可写流。
通俗的讲就是一个用户通过http协议访问一个服务器的时候,这个服务器会将一些key/value键值对返回给客户端浏览器,并给这些数据加上一些限制条件,在条件符合时这个用户下次访问这个服务器的时候,数据又被完整的带回给服务器。
set cookie: name=value; domain=.mozilla.org; expires=Feb, 13-Mar-2018 11:47:50; path=/; secure
此处需要额外注意的是,在C#中,如果一个cookie不设置对应的Domain,那么在CookieContainer.Add(cookies)的时候,会死掉。对于服务器返回的Set-Cookie中,如果没有指定Domain的值,那么其Domain的值是默认为当前所提交的http的请求所对应的主域名的。比如访问 [http://www.example.com],返回一个cookie,没有指名domain值,那么其为值为默认的www.example.com。
发向服务器的所有 cookie 的最大数量(空间)仍旧维持原始规范中所指出的:4KB。所有超出该限制的 cookie 都会被截掉并且不会发送至服务器。
由于cookie存在客户端,可能被篡改。
cookie本身并没有会话cookie和持久化cookie之分,只是取决于是否设置了存活时间。
cookie parsing middleware: github地址
$ npm install cookie-parser
var express = require('express')
var cookieParser = require('cookie-parser')
var app = express()
app.use(cookieParser())
cookieParser(secret, options)
secret:(String/Array)签名cookie,不指定则不会解析signed cookie
options:
decode: 用于解码cookie值的方法,默认为decodeURIComponent
session机制是一种服务器端的机制,服务器使用一种类似于散列表的结构(也可能就是使用散列表)来保存信息。
session 可以存放在 1)内存、2)cookie本身、3)redis 或 memcached 等缓存中,或者4)数据库中。
$ npm install express-session
var session = require('express-session')
session(option)
将session存储在redis中,可实现不同服务间的session共享
redis store : github地址(又见TJ...)
redis client : github地址
我的相关代码:
const cookieParser = require('cookie-parser');
const session = require('express-session');
const redisStore = require('connect-redis')(session);
const redis = require('redis');
//connect redis
const redisClient = redis.createClient();
redisClient.on('error',function(err){
logger.error(err);
process.exit(1);
});
app.use(cookieParser());
app.use(session({
store: new redisStore({
client:redisClient
}),
secret: 'ACDataServer',
cookie: {
maxAge: 12*60*60*1000
},
proxy: true,
saveUninitialized: true,
resave: false
}));
由于cookie可以被人为的禁止,必须有其他机制以便在cookie被禁止时仍然能够把session id传递回服务器。
这两种方式对于用户来说是没有区别的,只是服务器在解析的时候处理的方式不同,采用第一种方式也有利于把session id的信息和正常程序参数区分开来。
为了在整个交互过程中始终保持状态,就必须在每个客户端可能请求的路径后面都包含这个session id。
另一种技术叫做表单隐藏字段。就是服务器会自动修改表单,添加一个隐藏字段,以便在表单提交时能够把session id传递回服务器。比如下面的表单
<form name="testform" action="/xxx">
<input type="text">
</form>
在被传递给客户端之前将被改写成
<form name="testform" action="/xxx">
<input type="hidden" name="jsessionid" value="ByOK3vjFD75aPnrF7C2HmdnV6QZcEbzWoWiBYEnLerjQ99zWpBng!-145788764">
<input type="text">
</form>
wikipedia:
In programming languages, closures (also lexical closures or function closures) are techniques for implementing lexically scoped name binding in languages with first-class functions. Operationally, a closure is a record storing a function[a] together with an environment: a mapping associating each free variable of the function (variables that are used locally, but defined in an enclosing scope) with the value or reference to which the name was bound when the closure was created.
MDN:
闭包是指这样的作用域,它包含有一个函数 ,这个函数可以调用被这个作用域所封闭的变量、函数或者闭包等内容。通常我们通过闭包所对应的函数来获得对闭包的访问。
通俗的讲:
闭包就是一个能够访问外部变量的函数
在JS中,每个function都是闭包,因为它总是能访问在它外部定义的数据。
var generateClosure = function() {
var count = 0;
var get = function() {
count ++;
return count;
};
return get;
};
var counter = generateClosure();
console.log(counter()); // 输出 1
console.log(counter()); // 输出 2
console.log(counter()); // 输出 3
这段代码中, generateClosure() 函数中有一个局部变量count, 初值为 0。还有一个叫做 get 的函数, get 将其父作用域,也就是 generateClosure() 函数中的 count 变量增加 1,并返回 count 的值。 generateClosure() 的返回值是 get 函数。在外部我们通过 counter 变量调用了generateClosure() 函数并获取了它的返回值,也就是 get 函数,接下来反复调用几次 counter(),我们发现每次返回的值都递增了 1。
按照通常命令式编程思维的理解, count 是generateClosure 函数内部的变量,它的生命周期就是 generateClosure 被调用的时期,当 generateClosure 从调用栈中返回时, count 变量申请的空间也就被释放。问题是,在 generateClosure() 调用结束后, counter() 却引用了“已经释放了的” count变量,而且非但没有出错,反而每次调用 counter() 时还修改并返回了 count。
这正是所谓闭包的特性。当一个函数返回它内部定义的一个函数时,就产生了一个闭包,闭 包 不 但 包 括 被 返 回 的 函 数 , 还 包 括 这 个 函 数 的 定 义 环 境 。 上 面 例 子 中 ,当函数generateClosure() 的内部函数 get 被一个外部变量 counter 引用时, counter和generateClosure() 的局部变量就是一个闭包。
注:由于IE的js对象和DOM对象使用不同的垃圾收集方法,因此闭包在IE中会导致内存泄露问题,也就是无法销毁驻留在内存中的元素
Promise异常有两种捕获方式:一种是就近捕获,还有一种是最后catch()统一捕获。
Promise.resolve()
.then( () => {
// 使 .then() 返回一个 rejected promise
throw 'Oh no!';
})
.then( () => {
console.log( 'Not called.' );
}, reason => {
console.error( 'onRejected function called: ', reason );
});
只会捕获第一个then()中的异常,如第二个then()中抛出异常不会捕获。
优点:可以灵活处理每一个error;
缺点:如果有未捕获的异常,会报错: UnhandledPromiseRejectionWarning
Promise.resolve()
.then( () => {
// 使 .then() 返回一个 rejected promise
throw 'Oh no!';
})
.then( () => {
console.log( 'Not called.' );
})
.catch((reason) =>{
console.error('onRejected function called: ', reason);
});
catch()会捕获前面所有then()中抛出的异常。
优点:不会出现没有捕获的异常;
缺点: 进行个性化处理时会很复杂
建议:灵活混用这两种方法,需要特别处理时就近处理,不关注的error放在最后统一捕获处理。
纯函数是指不依赖于且不改变它作用域之外的变量状态的函数。
纯函数的返回值只由它调用时的参数决定,它的执行不依赖于系统的状态(比如:何时、何处调用它)
最主要的好处是没有副作用。纯函数不会修改作用域之外的状态,做到这一点,代码就变得足够简单和清晰:当你调用一个纯函数,你只要关注它的返回值,而不用担心因为别处的问题导致错误。
纯函数是健壮的,改变执行次序不会对系统造成影响,因此纯函数的操作可以并行执行。
纯函数非常容易进行单元测试,因为不需要考虑上下文环境,只需要考虑输入和输出。
最后,尽可能使用纯函数让你的代码保持简单和灵活。
典型应用1: Redux中的Reducer设计原则就是必须为纯函数
只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算。
典型应用2: React函数式无状态组件
相比于 class 创建组件
一个组件就是一个函数,函数应该是谈不上生命周期的,但是组件却是有生命周期,stateless functions 没有生命周期。当然了,我们其实可以使用 高阶组件 去实现生命周期
在 stateless functions 中,this 是 undefined,所以是不能使用 this 变量。不过换个角度思考,this 是在运行时随时可以被修改或重新赋值,跟外界环境有着密切的联系,正是不使用this才会让组件变得更纯。
const exec = require('child_process').exec;
exec('cat *.js bad_file | wc -l', (error, stdout, stderr) => {
if (error) {
console.error(`exec error: ${error}`);
return;
}
console.log(`stdout: ${stdout}`);
console.log(`stderr: ${stderr}`);
});
const spawn = require('child_process').spawn;
const ls = spawn('ls', ['-lh', '/usr']);
ls.stdout.on('data', (data) => {
console.log(`stdout: ${data}`);
});
ls.stderr.on('data', (data) => {
console.log(`stderr: ${data}`);
});
ls.on('close', (code) => {
console.log(`child process exited with code ${code}`);
});
exec()默认参数:
const defaults = {
encoding: 'utf8',
timeout: 0,
maxBuffer: 200*1024,
killSignal: 'SIGTERM',
cwd: null,
env: null
};
spawn()默认参数:
const defaults = {
cwd: undefined,
env: process.env
};
详见源码:
exports.exec =
function(command /*, options, callback*/) {
var opts = normalizeExecArgs.apply(null, arguments);
return exports.execFile(opts.file,
opts.args,
opts.options,
opts.callback);
};
exec对于execFile的封装是进行参数处理
处理的函数:
normalizeExecArgs
if (process.platform === 'win32') {
file = process.env.comspec || 'cmd.exe';
args = ['/s', '/c', '"' + command + '"'];
// Make a shallow copy before patching so we don't clobber the user's
// options object.
options = util._extend({}, options);
options.windowsVerbatimArguments = true;
} else {
file = '/bin/sh';
args = ['-c', command];
}
感觉这个处理和我的想法很像...
将简单的command命名做一个,win和linux的平台处理。
此时execFile接受到的就是一个区分平台的command参数。
然后重点来了,继续debug,execFile中:
var options = {
encoding: 'utf8',
timeout: 0,
maxBuffer: 200 * 1024,
killSignal: 'SIGTERM',
cwd: null,
env: null
};
有这么一段,设置了默认的参数。然后后面又是一些参数处理,最后调用spawn方法启动子进程。
上面的简单流程就是启动一个子进程。到这里都没有什么问题。
继续看,重点又来了:
用过子进程应该知道这个child.stderr
下面的代码就解答了为什么子进程会挂掉。
child.stderr.addListener('data', function(chunk) {
stderrLen += chunk.length;
if (stderrLen > options.maxBuffer) {
ex = new Error('stderr maxBuffer exceeded.');
kill();
} else {
if (!encoding)
_stderr.push(chunk);
else
_stderr += chunk;
}
});
逻辑就是,记录子进程的log大小,一旦超过maxBuffer就kill掉子进程。
原来真相在这里。我们在使用exec时,不知道设置maxBuffer,默认的maxBuffer是200K,当我们子进程日志达到200K时,自动kill()掉了。
这也正是exec()与spawn()最大的区别: exec()限制了maxBuffer默认为200k,超过时会自动kill子进程,并报"maxBuffer exceeded"异常
参考文献:https://www.hacksparrow.com/difference-between-spawn-and-exec-of-node-js-child_process.html
根据程序上下文环境,Java关键字final有“这是无法改变的”或者“终态的”含义,它可以修饰非抽象类、非抽象类成员方法和变量。你可能出于两种理解而需要阻止改变:设计或效率。
final类不能被继承,没有子类,final类中的方法默认是final的。
final方法不能被子类的方法覆盖,但可以被继承。
final成员变量表示常量,只能被赋值一次,赋值后值不再改变。
final不能用于修饰构造方法。
注意:父类的private成员方法是不能被子类方法覆盖的,因此private类型的方法默认是final类型的。
final类不能被继承,因此final类的成员方法没有机会被覆盖,默认都是final的。在设计类时候,如果这个类不需要有子类,类的实现细节不允许改变,并且确信这个类不会载被扩展,那么就设计为final类。
如果一个类不允许其子类覆盖某个方法,则可以把这个方法声明为final方法。
使用final方法的原因有二:
第一、把方法锁定,防止任何继承类修改它的意义和实现。
第二、高效。编译器在遇到调用final方法时候会转入内嵌机制,大大提高执行效率。
例如:
public class Test1 {
public static void main(String[] args) {
// TODO 自动生成方法存根
}
public void f1() {
System.out.println("f1");
}
//无法被子类覆盖的方法
public final void f2() {
System.out.println("f2");
}
public void f3() {
System.out.println("f3");
}
private void f4() {
System.out.println("f4");
}
}
public class Test2 extends Test1 {
public void f1(){
System.out.println("Test1父类方法f1被覆盖!");
}
public static void main(String[] args) {
Test2 t=new Test2();
t.f1();
t.f2(); //调用从父类继承过来的final方法
t.f3(); //调用从父类继承过来的方法
//t.f4(); //调用失败,无法从父类继承获得
}
}
用final修饰的成员变量表示常量,值一旦给定就无法改变!
final修饰的变量有三种:静态变量、实例变量和局部变量,分别表示三种类型的常量。
从下面的例子中可以看出,一旦给final变量初值后,值就不能再改变了。
另外,final变量定义的时候,可以先声明,而不给初值,这中变量也称为final空白,无论什么情况,编译器都确保空白final在使用之前必须被初始化。但是,final空白在final关键字final的使用上提供了更大的灵活性,为此,一个类中的final数据成员就可以实现依对象而有所不同,却有保持其恒定不变的特征。
package org.leizhimin;
public class Test3 {
private final String S = "final实例变量S";
private final int A = 100;
public final int B = 90;
public static final int C = 80;
private static final int D = 70;
public final int E; //final空白,必须在初始化对象的时候赋初值
public Test3(int x) {
E = x;
}
/**
* @param args
*/
public static void main(String[] args) {
Test3 t = new Test3(2);
//t.A=101; //出错,final变量的值一旦给定就无法改变
//t.B=91; //出错,final变量的值一旦给定就无法改变
//t.C=81; //出错,final变量的值一旦给定就无法改变
//t.D=71; //出错,final变量的值一旦给定就无法改变
System.out.println(t.A);
System.out.println(t.B);
System.out.println(t.C); //不推荐用对象方式访问静态字段
System.out.println(t.D); //不推荐用对象方式访问静态字段
System.out.println(Test3.C);
System.out.println(Test3.D);
//System.out.println(Test3.E); //出错,因为E为final空白,依据不同对象值有所不同.
System.out.println(t.E);
Test3 t1 = new Test3(3);
System.out.println(t1.E); //final空白变量E依据对象的不同而不同
}
private void test() {
System.out.println(new Test3(1).A);
System.out.println(Test3.C);
System.out.println(Test3.D);
}
public void test2() {
final int a; //final空白,在需要的时候才赋值
final int b = 4; //局部常量--final用于局部变量的情形
final int c; //final空白,一直没有给赋值.
a = 3;
//a=4; 出错,已经给赋过值了.
//b=2; 出错,已经给赋过值了.
}
}
当函数参数为final类型时,你可以读取使用该参数,但是无法改变该参数的值。
public class Test4 {
public static void main(String[] args) {
new Test4().f1(2);
}
public void f1(final int i) {
//i++; //i是final类型的,值不允许改变的.
System.out.print(i);
}
}
static表示“全局”或者“静态”的意思,用来修饰成员变量和成员方法,也可以形成静态static代码块,但是Java语言中没有全局变量的概念。
被static修饰的成员变量和成员方法独立于该类的任何对象。也就是说,它不依赖类特定的实例,被类的所有实例共享。只要这个类被加载,Java虚拟机就能根据类名在运行时数据区的方法区内定找到他们。因此,static对象可以在它的任何对象创建之前访问,无需引用任何对象。
用public修饰的static成员变量和成员方法本质是全局变量和全局方法,当声明它类的对象市,不生成static变量的副本,而是类的所有实例共享同一个static变量。
static变量前可以有private修饰,表示这个变量可以在类的静态代码块中,或者类的其他静态成员方法中使用(当然也可以在非静态成员方法中使用--废话),但是不能在其他类中通过类名来直接引用,这一点很重要。实际上你需要搞明白,private是访问权限限定,static表示不要实例化就可以使用,这样就容易理解多了。static前面加上其它访问权限关键字的效果也以此类推。
static修饰的成员变量和成员方法习惯上称为静态变量和静态方法,可以直接通过类名来访问,访问语法为:
类名.静态方法名(参数列表...)
类名.静态变量名
用static修饰的代码块表示静态代码块,当Java虚拟机(JVM)加载类时,就会执行该代码块(用处非常大,呵呵)。
按照是否静态的对类成员变量进行分类可分两种:一种是被static修饰的变量,叫静态变量或类变量;另一种是没有被static修饰的变量,叫实例变量。两者的区别是:
对于静态变量在内存中只有一个拷贝(节省内存),JVM只为静态分配一次内存,在加载类的过程中完成静态变量的内存分配,可用类名直接访问(方便),当然也可以通过对象来访问(但是这是不推荐的)。
对于实例变量,没创建一个实例,就会为实例变量分配一次内存,实例变量可以在内存中有多个拷贝,互不影响(灵活)。
静态方法可以直接通过类名调用,任何的实例也都可以调用,因此静态方法中不能用this和super关键字,不能直接访问所属类的实例变量和实例方法(就是不带static的成员变量和成员成员方法),只能访问所属类的静态成员变量和成员方法。因为实例成员与特定的对象关联!这个需要去理解,想明白其中的道理,不是记忆!!!
因为static方法独立于任何实例,因此static方法必须被实现,而不能是抽象的abstract。
static代码块也叫静态代码块,是在类中独立于类成员的static语句块,可以有多个,位置可以随便放,它不在任何的方法体内,JVM加载类时会执行这些静态的代码块,如果static代码块有多个,JVM将按照它们在类中出现的先后顺序依次执行它们,每个代码块只会被执行一次。例如:
public class Test5 {
private static int a;
private int b;
static {
Test5.a = 3;
System.out.println(a);
Test5 t = new Test5();
t.f();
t.b = 1000;
System.out.println(t.b);
}
static {
Test5.a = 4;
System.out.println(a);
}
public static void main(String[] args) {
// TODO 自动生成方法存根
}
static {
Test5.a = 5;
System.out.println(a);
}
public void f() {
System.out.println("hhahhahah");
}
}
运行结果:
3
hhahhahah
1000
4
5
利用静态代码块可以对一些static变量进行赋值,最后再看一眼这些例子,都一个static的main方法,这样JVM在运行main方法的时候可以直接调用而不用创建实例。
static final用来修饰成员变量和成员方法,可简单理解为“全局常量”!
对于变量,表示一旦给值就不可修改,并且通过类名可以访问。
对于方法,表示不可覆盖,并且可以通过类名直接访问。
特别要注意一个问题:
对于被static和final修饰过的实例常量,实例本身不能再改变了,但对于一些容器类型(比如,ArrayList、HashMap)的实例变量,不可以改变容器变量本身,但可以修改容器中存放的对象,这一点在编程中用到很多。
也许说了这么多,反倒把你搞晕了,还是看个例子吧:
public class TestStaticFinal {
private static final String strStaticFinalVar = "aaa";
private static String strStaticVar = null;
private final String strFinalVar = null;
private static final int intStaticFinalVar = 0;
private static final Integer integerStaticFinalVar = new Integer(8);
private static final ArrayList<String> alStaticFinalVar = new ArrayList<String>();
private void test() {
System.out.println("-------------值处理前----------\r\n");
System.out.println("strStaticFinalVar=" + strStaticFinalVar + "\r\n");
System.out.println("strStaticVar=" + strStaticVar + "\r\n");
System.out.println("strFinalVar=" + strFinalVar + "\r\n");
System.out.println("intStaticFinalVar=" + intStaticFinalVar + "\r\n");
System.out.println("integerStaticFinalVar=" + integerStaticFinalVar + "\r\n");
System.out.println("alStaticFinalVar=" + alStaticFinalVar + "\r\n");
//strStaticFinalVar="哈哈哈哈"; //错误,final表示终态,不可以改变变量本身.
strStaticVar = "哈哈哈哈"; //正确,static表示类变量,值可以改变.
//strFinalVar="呵呵呵呵"; //错误, final表示终态,在定义的时候就要初值(哪怕给个null),一旦给定后就不可再更改。
//intStaticFinalVar=2; //错误, final表示终态,在定义的时候就要初值(哪怕给个null),一旦给定后就不可再更改。
//integerStaticFinalVar=new Integer(8); //错误, final表示终态,在定义的时候就要初值(哪怕给个null),一旦给定后就不可再更改。
alStaticFinalVar.add("aaa"); //正确,容器变量本身没有变化,但存放内容发生了变化。这个规则是非常常用的,有很多用途。
alStaticFinalVar.add("bbb"); //正确,容器变量本身没有变化,但存放内容发生了变化。这个规则是非常常用的,有很多用途。
System.out.println("-------------值处理后----------\r\n");
System.out.println("strStaticFinalVar=" + strStaticFinalVar + "\r\n");
System.out.println("strStaticVar=" + strStaticVar + "\r\n");
System.out.println("strFinalVar=" + strFinalVar + "\r\n");
System.out.println("intStaticFinalVar=" + intStaticFinalVar + "\r\n");
System.out.println("integerStaticFinalVar=" + integerStaticFinalVar + "\r\n");
System.out.println("alStaticFinalVar=" + alStaticFinalVar + "\r\n");
}
public static void main(String args[]) {
new TestStaticFinal().test();
}
}
运行结果如下:
-------------值处理前----------
strStaticFinalVar=aaa
strStaticVar=null
strFinalVar=null
intStaticFinalVar=0
integerStaticFinalVar=8
alStaticFinalVar=[]
-------------值处理后----------
strStaticFinalVar=aaa
strStaticVar=哈哈哈哈
strFinalVar=null
intStaticFinalVar=0
integerStaticFinalVar=8
alStaticFinalVar=[aaa, bbb]
Process finished with exit code 0
看了上面这个例子,就清楚很多了,但必须明白:通过static final修饰的容器类型变量中所“装”的对象是可改变的。这是和一般基本类型和类类型变量差别很大的地方。
例:
用户的使用方式复杂
不同身份的用户有不同的使用方式(比如普通用户和管理员)
多个用户之间可以协作
与服务器大量交互,或者使用了WebSocket
View要从多个来源获取数据
整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。
惟一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件
的普通对象。
把数据从应用传到store的有效载荷,是store的唯一数据来源。
我们应该尽量减少在Action中传递的数据。
store里能直接通过store.dispath()调用dispatch()方法,但是大多数情况下会使用react-redux提供的
connect()帮助器来调用。bindActionCreators()函数可以把多个action创建函数绑定到dispatch()方法上。
指明应用如何更新state。
在Redux应用中,所有state都被保存在一个单一对象中。建议在写代码前先向下这个对象的结构。
开发复杂的应用时, 不可避免会有一些数据相互引用。 建议你尽可能地把 state 范式化, 不存在嵌套。 把所有数据放到一个对象里, 每个数据以 ID 为主键, 不同实体或列表间通过 ID 相互引⽤数据。 把应用的state 想像成数据库。
Reducer是一个纯函数,接收旧的state和action,返回新的state。
永远不要在reducer中做这些操作:
只要传入参数相同, 返回计算得到的下一个 state 就一定相同。 没有特殊情况、 没有副作用, 没有 API 请求、 没有变量修改, 单纯执行计算。
注意每个 reducer 只负责管理全局 state 中它负责的一部分。每个 reducer 的 state 参数都不同,分别对应它管理的那部分 state 数据。
Store是将Action和Reducer联系起来的。职责:
import { createStore } from 'redux'
import todoApp from './reducers'
let store = createStore(todoApp)
严格的单向数据流是 Redux 架构的设计核心。
Redux 应用中数据的生命周期遵循下面 4 个步骤:
这个新的树就是应用的下一个 state!所有订阅 store.subscribe(listener) 的监听器都将被调用;监听器里可以调用 store.getState() 获得当前 state。
import React, { PropTypes } from 'react';
import { Provider } from 'react-redux';
import { Router, Route, browserHistory } from 'react-router';
import App from './App';
const Root = ({ store }) => (
<Provider store={store}>
<Router history={browserHistory}>
<Route path="/(:filter)" component={App} />
</Router>
</Provider>
);
Root.propTypes = {
store: PropTypes.object.isRequired,
};
export default Root;
ES5中的set通常如此表示:
var set = Object.create(null);
set.foo = true;
// checking for existence
if (set.foo) {
// code to execute
}
ES6:
let set = new Set();
set.add(5);
set.add("5");
console.log(set.size); // 2
ES6的Set去重内部使用的是Object.is()方法,所以5和'5'是两个不同的子元素。
Set方法包括:
可以利用set的特性对数组进行去重:
function eliminateDuplicates(items) {
return [...new Set(items)];
}
let numbers = [1, 2, 3, 3, 3, 4, 5],
noDuplicates = eliminateDuplicates(numbers);
console.log(noDuplicates); // [1,2,3,4,5]
Set存在的问题:
let set = new Set(),
key = {};
set.add(key);
console.log(set.size); // 1
// eliminate original reference
key = null;
console.log(set.size); // 1
// get the original reference back
key = [...set][0];
set中的对象元素置空后,仍然可以通过set获取到元素原来的引用,这样容易造成内存泄漏。
于是ES6引入了WeakSet。
let set = new WeakSet(),
key = {};
// add the object to the set
set.add(key);
console.log(set.has(key)); // true
// remove the last strong reference to key (also removes from weak set)
key = null;
当WeakSet中的对象元素置空后,WeakSet中的元素也被移除了。
注意: WeakSet中只能保存对象元素
WeakSet与Set的不同:
ES5 中的 Map实现:
var map = Object.create(null);
map.foo = "bar";
// retrieving a value
var value = map.foo;
console.log(value); // "bar"
ES5中map实现的问题:
var map = Object.create(null);
map[5] = "foo";
console.log(map["5"]); // "foo"
var map = Object.create(null),
key1 = {},
key2 = {};
map[key1] = "foo";
console.log(map[key2]); // "foo"
在ES5中,会自动将数字key转换为string,所以map[5]和map['5']的值是一样的。
key1和key2的value值相同,因为key1和key2被转换为string,对象属性必须是string,因为[object Object]是对象的默认string值,所以key1和key2都转换为这个string值,所以他们的value相同。
ES6 Map:
let map = new Map();
map.set("title", "Understanding ECMAScript 6");
map.set("year", 2016);
console.log(map.get("title")); // "Understanding ECMAScript 6"
console.log(map.get("year")); // 2016
在ES6的Map中每一个对象都被看成是唯一的。
Map方法:
Map的key也存在和Set同样的内存泄漏隐患。
let map = new WeakMap(),
element = document.querySelector(".element");
map.set(element, "Original");
let value = map.get(element);
console.log(value); // "Original"
// remove the element
element.parentNode.removeChild(element);
element = null;
// the weak map is empty at this point
WeakMap的key只能为Object
WeakMap的方法:
当 Web 资源请求由其它域名或端口提供的资源时,会发起跨域 HTTP 请求(cross-origin HTTP request)。比如,站点 http://domain-a.com 的某 HTML 页面通过 的 src 请求 http://domain-b.com/image.jpg。
出于安全考虑,浏览器会限制脚本中发起的跨域请求。比如,使用 XMLHttpRequest 和 Fetch 发起的 HTTP 请求必须遵循同源策略。
同源:如果协议,端口(如果指定了一个)和域名对于两个页面是相同的,则两个页面具有相同的源。
浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。简单请求不会触发CORS预检请求。
若请求满足所有下述条件,则该请求可视为“简单请求”:
当请求满足下述任一条件时,即应首先发送预检请求:
比如说,假如站点 http://foo.example 的网页应用想要访问 http://bar.other 的资源。http://foo.example 的网页中可能包含类似于下面的 JavaScript 代码:
var invocation = new XMLHttpRequest();
var url = 'http://bar.other/resources/public-data/';
function callOtherDomain() {
if(invocation) {
invocation.open('GET', url, true);
invocation.onreadystatechange = handler;
invocation.send();
}
}
客户端和服务器之间使用 CORS 首部字段来处理跨域权限:
分别检视请求报文和响应报文:
GET /resources/public-data/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Referer: http://foo.example/examples/access-control/simpleXSInvocation.html
Origin: http://foo.example
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 00:23:53 GMT
Server: Apache/2.0.61
Access-Control-Allow-Origin: *
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: application/xml
[XML Data]
第 1~10 行是请求首部。第10行 的请求首部字段 Origin 表明该请求来源于 http://foo.exmaple。
第 13~22 行是来自于 http://bar.other 的服务端响应。响应中携带了响应首部字段 Access-Control-Allow-Origin(第 16 行)。使用 Origin 和 Access-Control-Allow-Origin 就能完成最简单的访问控制。本例中,服务端返回的 Access-Control-Allow-Origin: * 表明,该资源可以被任意外域访问。如果服务端仅允许来自 http://foo.example 的访问,该首部字段的内容如下:
Access-Control-Allow-Origin: http://foo.example
var invocation = new XMLHttpRequest();
var url = 'http://bar.other/resources/post-here/';
var body = 'Arun';
function callOtherDomain(){
if(invocation)
{
invocation.open('POST', url, true);
invocation.setRequestHeader('X-PINGOTHER', 'pingpong');
invocation.setRequestHeader('Content-Type', 'application/xml');
invocation.onreadystatechange = handler;
invocation.send(body);
}
}
上面的代码使用 POST 请求发送一个 XML 文档,该请求包含了一个自定义的请求首部字段(X-PINGOTHER: pingpong)。另外,该请求的 Content-Type 为 application/xml。因此,该请求需要首先发起“预检请求”。
OPTIONS /resources/post-here/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Origin: http://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
预检请求完成之后,发送实际请求:
POST /resources/post-here/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
X-PINGOTHER: pingpong
Content-Type: text/xml; charset=UTF-8
Referer: http://foo.example/examples/preflightInvocation.html
Content-Length: 55
Origin: http://foo.example
Pragma: no-cache
Cache-Control: no-cache
<?xml version="1.0"?><person><name>Arun</name></person>
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:40 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://foo.example
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 235
Keep-Alive: timeout=2, max=99
Connection: Keep-Alive
Content-Type: text/plain
[Some GZIP'd payload]
浏览器检测到,从 JavaScript 中发起的请求需要被预检。从上面的报文中,我们看到,第 1~12 行发送了一个使用 OPTIONS 方法的“预检请求”。 OPTIONS 是 HTTP/1.1 协议中定义的方法,用以从服务器获取更多信息。该方法不会对服务器资源产生影响。 预检请求中同时携带了下面两个首部字段:
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER
首部字段 Access-Control-Request-Method 告知服务器,实际请求将使用 POST 方法。首部字段 Access-Control-Request-Headers 告知服务器,实际请求将携带两个自定义请求首部字段:X-PINGOTHER 与 Content-Type。服务器据此决定,该实际请求是否被允许。
第1426 行为预检请求的响应,表明服务器将接受后续的实际请求。重点看第 1720 行:
Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
首部字段 Access-Control-Allow-Methods 表明服务器允许客户端使用 POST, GET 和 OPTIONS 方法发起请求。该字段与 HTTP/1.1 Allow: response header 类似,但仅限于在需要访问控制的场景中使用。
首部字段 Access-Control-Allow-Headers 表明服务器允许请求中携带字段 X-PINGOTHER 与 Content-Type。与 Access-Control-Allow-Methods 一样,Access-Control-Allow-Headers 的值为逗号分割的列表。
最后,首部字段 Access-Control-Max-Age 表明该响应的有效时间为 86400 秒,也就是 24 小时。在有效时间内,浏览器无须为同一请求再次发起预检请求。请注意,浏览器自身维护了一个最大有效时间,如果该首部字段的值超过了最大有效时间,将不会生效。
Fetch 与 CORS 的一个有趣的特性是,可以基于 HTTP cookies 和 HTTP 认证信息发送身份凭证。一般而言,对于跨域 XMLHttpRequest 或 Fetch 请求,浏览器不会发送身份凭证信息。如果要发送凭证信息,需要设置 XMLHttpRequest 的某个特殊标志位。
本例中,http://foo.example 的某脚本向 http://bar.other 发起一个GET 请求,并设置 Cookies:
var invocation = new XMLHttpRequest();
var url = 'http://bar.other/resources/credentialed-content/';
function callOtherDomain(){
if(invocation) {
invocation.open('GET', url, true);
invocation.withCredentials = true;
invocation.onreadystatechange = handler;
invocation.send();
}
}
第 7 行将 XMLHttpRequest 的 withCredentials 标志设置为 true,从而向服务器发送 Cookies。因为这是一个简单 GET 请求,所以浏览器不会对其发起“预检请求”。但是,如果服务器端的响应中未携带 Access-Control-Allow-Credentials: true ,浏览器将不会把响应内容返回给请求的发送者。
客户端与服务器端交互示例如下:
GET /resources/access-control-with-credentials/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Referer: http://foo.example/examples/credential.html
Origin: http://foo.example
Cookie: pageAccess=2
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:34:52 GMT
Server: Apache/2.0.61 (Unix) PHP/4.4.7 mod_ssl/2.0.61 OpenSSL/0.9.7e mod_fastcgi/2.4.2 DAV/2 SVN/1.4.2
X-Powered-By: PHP/5.2.6
Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Credentials: true
Cache-Control: no-cache
Pragma: no-cache
Set-Cookie: pageAccess=3; expires=Wed, 31-Dec-2008 01:34:53 GMT
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 106
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
[text/plain payload]
即使第 11 行指定了 Cookie 的相关信息,但是,如果 bar.other 的响应中缺失 Access-Control-Allow-Credentials: true(第 19 行),则响应内容不会返回给请求的发起者。
附带身份凭证的请求与通配符
对于附带身份凭证的请求,服务器不得设置 Access-Control-Allow-Origin 的值为“*”。
这是因为请求的首部中携带了 Cookie 信息,如果 Access-Control-Allow-Origin 的值为“*”,请求将会失败。而将 Access-Control-Allow-Origin 的值设置为 http://foo.example,则请求将成功执行。
另外,响应首部中也携带了 Set-Cookie 字段,尝试对 Cookie 进行修改。如果操作失败,将会抛出异常。
响应首部中可以携带一个 Access-Control-Allow-Origin 字段,其语法如下:
Access-Control-Allow-Origin: | *
其中,origin 参数的值指定了允许访问该资源的外域 URI。对于不需要携带身份凭证的请求,服务器可以指定该字段的值为通配符,表示允许来自所有域的请求。
例如,下面的字段值将允许来自 http://mozilla.com 的请求:
Access-Control-Allow-Origin: http://mozilla.com
如果服务端指定了具体的域名而非“*”,那么响应首部中的 Vary 字段的值必须包含 Origin。这将告诉客户端:服务器对不同的源站返回不同的内容。
Access-Control-Expose-Headers 首部字段指定了服务端允许的首部字段集合。
Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header
服务器允许请求中携带 X-My-Custom-Header 和 X-Another-Custom-Header 这两个字段。
Access-Control-Max-Age 首部字段指明了预检请求的响应的有效时间。
Access-Control-Max-Age:
delta-seconds 表示该响应在多少秒内有效。
Access-Control-Allow-Credentials 首部字段用于预检请求的响应,表明服务器是否允许 credentials 标志设置为 true 的请求。请注意:简单 GET 请求不会被预检;如果对此类请求的响应中不包含该字段,浏览器不会将响应返回给请求的调用者。
Access-Control-Allow-Credentials: true
上文已经讨论了附带身份凭证的请求。
Access-Control-Allow-Methods 首部字段用于预检请求的响应。其指明了实际请求所允许使用的 HTTP 方法。
Access-Control-Allow-Methods: <method>[, <method>]*
Access-Control-Allow-Headers 首部字段用于预检请求的响应。其指明了实际请求中允许携带的首部字段。
Access-Control-Allow-Headers: <field-name>[, <field-name>]*
Origin 首部字段表明预检请求或实际请求的源站。
Origin: <origin>
origin 参数的值为源站 URI。它不包含任何路径信息,只是服务器名称。
Note: 有时候将该字段的值设置为空字符串是有用的,例如,当源站是一个 data URL 时。
注意,不管是否为跨域请求,ORIGIN 字段总是被发送。
Access-Control-Request-Method 首部字段用于预检请求。其作用是,将实际请求所使用的 HTTP 方法告诉服务器。
Access-Control-Request-Method: <method>
相关示例见这里。
Access-Control-Request-Headers 首部字段用于预检请求。其作用是,将实际请求所携带的首部字段告诉服务器。
Access-Control-Request-Headers: <field-name>[, <field-name>]*
以上内容主要摘自 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS
接下来是关于node.js中跨域中间件
Express cors middleware
$ npm install cors
为所有的请求设置跨域:
var express = require('express')
var cors = require('cors')
var app = express()
app.use(cors())
app.get('/products/:id', function (req, res, next) {
res.json({msg: 'This is CORS-enabled for all origins!'})
})
app.listen(80, function () {
console.log('CORS-enabled web server listening on port 80')
})
为特定路由设置跨域:
var express = require('express')
var cors = require('cors')
var app = express()
app.get('/products/:id', cors(), function (req, res, next) {
res.json({msg: 'This is CORS-enabled for a Single Route'})
})
app.listen(80, function () {
console.log('CORS-enabled web server listening on port 80')
})
配置跨域参数:
var express = require('express')
var cors = require('cors')
var app = express()
var corsOptions = {
origin: 'http://example.com',
optionsSuccessStatus: 200 // some legacy browsers (IE11, various SmartTVs) choke on 204
}
app.get('/products/:id', cors(corsOptions), function (req, res, next) {
res.json({msg: 'This is CORS-enabled for only example.com.'})
})
app.listen(80, function () {
console.log('CORS-enabled web server listening on port 80')
})
配置动态origin参数:
var express = require('express')
var cors = require('cors')
var app = express()
var whitelist = ['http://example1.com', 'http://example2.com']
var corsOptions = {
origin: function (origin, callback) {
if (whitelist.indexOf(origin) !== -1) {
callback(null, true)
} else {
callback(new Error('Not allowed by CORS'))
}
}
}
app.get('/products/:id', cors(corsOptions), function (req, res, next) {
res.json({msg: 'This is CORS-enabled for a whitelisted domain.'})
})
app.listen(80, function () {
console.log('CORS-enabled web server listening on port 80')
})
允许预检请求:
var express = require('express')
var cors = require('cors')
var app = express()
app.options('/products/:id', cors()) // enable pre-flight request for DELETE request
app.del('/products/:id', cors(), function (req, res, next) {
res.json({msg: 'This is CORS-enabled for all origins!'})
})
app.listen(80, function () {
console.log('CORS-enabled web server listening on port 80')
});
或者为所有路由设置预检请求:
app.options('*', cors()) // include before other routes
异步配置跨域:
var express = require('express')
var cors = require('cors')
var app = express()
var whitelist = ['http://example1.com', 'http://example2.com']
var corsOptionsDelegate = function (req, callback) {
var corsOptions;
if (whitelist.indexOf(req.header('Origin')) !== -1) {
corsOptions = { origin: true } // reflect (enable) the requested origin in the CORS response
}else{
corsOptions = { origin: false } // disable CORS for this request
}
callback(null, corsOptions) // callback expects two parameters: error and options
}
app.get('/products/:id', cors(corsOptionsDelegate), function (req, res, next) {
res.json({msg: 'This is CORS-enabled for a whitelisted domain.'})
})
app.listen(80, function () {
console.log('CORS-enabled web server listening on port 80')
})
origin:配置Access-Control-Allow-Origin首部,可能的值:
Boolean:true表示允许来源URI访问,false表示禁用CORS跨域
String:指定一个URI可以跨域访问此域
RegExp:符合正则表达式的域可以访问
Array:设置一组可以访问域的数组
Function:自定义逻辑设置函数,第一个参数为来源的uri,第二个参数为一个回调函数callback(err,boolean)
methods:设置 Access-Control-Allow-Methods首部,例如:字符串('GET,PUT,POST')或数组
(['GET','PUT','POST'])
allowedHeaders:设置Access-Control-Allow-Headers 首部,用于预检请求的响应,(ex: 'Content-Type,Authorization')或(ex: ['Content-Type', 'Authorization']),如果不指定则反射请求中的Access-Control-Request-Headers首部
exposedHeaders: 设置 Access-Control-Expose-Headers 首部,指定了服务端允许的首部字段集合, (ex: 'Content-Range,X-Content-Range') 或 (ex: ['Content-Range', 'X-Content-Range']).
credentials: 设置 Access-Control-Allow-Credentials 首部,设为true启用
maxAge:设置 Access-Control-Max-Age首部,int
preflightContinue: 将预检请求传递给下一个处理器;
为成功的请求提供一个状态码
默认的配置选项:
{
"origin": "*",
"methods": "GET,HEAD,PUT,PATCH,POST,DELETE",
"preflightContinue": false,
"optionsSuccessStatus": 204
}
对累加器和数组的每个值 (从左到右)应用一个函数,以将其减少为单个值。
例1:
let sum = [0, 1, 2, 3].reduce(function(acc, val) {
return acc + val;
}, 0);
console.log(sum);
// 6
例2(TJ—only):
//Return whitelisted properties of an object
//筛选obj中特定的properties,reduce合并为一个新json对象返回
module.exports = function(obj, keys){
obj = obj || {};
if ('string' == typeof keys) keys = keys.split(/ +/);
return keys.reduce(function(ret, key){
if (null == obj[key]) return ret;
ret[key] = obj[key];
return ret;
}, {});
};
var obj = {
name: 'tobi',
last: 'holowaychuk',
email: '[email protected]',
_id: '12345'
};
var user = only(obj, 'name last email');
用于将所有可枚举的属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。
Object.assign(target, ...sources)
参数:
target 目标对象。
sources (多个)源对象。
返回值:目标对象。
Object.assign 方法只会拷贝源对象自身的并且可枚举的属性到目标对象身上。
针对深度拷贝,需要使用其他方法,因为 Object.assign() 拷贝的是属性值。假如源对象的属性值是一个指向对象的引用,它也只拷贝那个引用值。
Object.is() 方法确定两个值是否是 相同的值。
Object.is(value1, value2);
参数
value1 需要比较的第一个值。
value2 需要比较的第二个值。
返回值
一个布尔值指示两个参数是否相同的。
Object.is() 会在下面这些情况下认为两个值是相同的:
== 运算符会对它两边的操作数做隐式的类型转换(如果它们是不同类型的值的话),然后才进行相等性比较,(所以才会有类似 "" == false 为 true 的现象),但 Object.is 不会做这种类型转换。
严格相等运算符 === 也不会对操作数进行类型转换,但是它会把 -0 和 +0 这两个数值视为相同的,还会把两个 NaN 看成是不相等的。
Object.is(5,'5'); //false
5 == '5' //true
5 === '5' //false
Object.is(NaN,NaN); //true
NaN === NaN //false
Object.is(-0,+0); //false
-0 === +0 //true
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
传统的Session认证通常是将sessionId保存在客户端的cookie中,session存储在服务器端的内存或redis等中。当请求时,通过sessionId与服务器端的session进行对比认证。
一句话概括与Session认证的区别:jwt只需保存在客户端,session认证需要将sessionId保存在cookie中,session保存在服务器中。
流程上是这样的:
第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature).
jwt的头部承载两部分信息:
完整的头部就像下面这样的JSON:
{
'typ': 'JWT',
'alg': 'HS256'
}
然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分.
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分
标准中注册的声明 (建议但不强制使用) :
公共的声明 :
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
私有的声明 :
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
定义一个payload:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
然后将其进行base64加密,得到Jwt的第二部分。
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。
// javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, 'secret'); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
将这三部分用.连接成一个完整的字符串,构成了最终的jwt:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW
express jwt认证中间件
github主页:https://github.com/auth0/express-jwt
server.js
app.use(expressJwt({secret:"secret"}).unless({path:['/login']}));
app.use(function(err,req,res,next){
if(err.name === 'UnauthorizedError'){
console.log(err);
return res.status(401).send("invalid token");
}
next();
});
客户端应用
一般是在请求头里加入Authorization,并加上Bearer标注:
fetch('api/user/1', {
headers: {
'Authorization': 'Bearer ' + token
}
})
生成jwt
github主页:https://github.com/auth0/node-jsonwebtoken
Usage
jwt.sign(payload, secretOrPrivateKey, [options, callback])
var authToken = jwt.sign({username: username}, "secret");
git commit --amend
修改上次commit,然后再一次arc diff
来更新上次的diff。 原则上没被accept的diff不允许push到remote。提交步骤:
git add(新文件) --> arc diff --> arc land
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.