通过HTTP的HEADER完成各种骚操作

作为一名专业的切图工程师,我从来不care网页的header,最多关心Status Code是不是200。但是HEADER真的很重要啊,客户端从服务器端获取内容,首先就是通过HEADER进行各种沟通!HEADER可以帮助我们完成许多骚操作,提高网站的性能,用户的体验。好了让我们来feel一下。

初级骚操作

  • 多语言(Accept-Language
  • 防盗链(RefererReferered
  • gzip,简单地说就是省流量(Accept-EncodingContent-Encoding

多语言

多语言就是一个网站可以实现多种语言的切换,这里不讨论建N个网站,一个语言对应一个网站。这里讨论如何智能返回用户所需的语言。

server client
向server扔过去了Accept-Language
接收对方的Accept-Language
字段大概这样子zh,en-US;q=0.9,en;q=0.8
开始处理,将字段变成带权重q的数组
排序好大概长这样[{"name":"zh","q":1},{"name":"en-US","q":0.9},{"name":"en","q":0.8}]
根据权重返回拥有的语言,有zh返回zh,没有zh就返回en-US
万一我没有对方需要的语言包,怎么办?急,在线等!
没办法了,只能给对方我们的官方(默认)语言
发送,请接收
您的ACCEPT语言已匹配 这个网站挺上道的,虽然是国外网站,但知道我是中文
我们没有你所在地区的语言包 emmmm,这是火星文吗?

附赠多语言的简易实现版:

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
let languages = {
zh:{
title:"你好",
content:"同学"
},
en:{
title:"Hey",
content:"guy"
},
}
//设置默认语言,万一用户的语言我们不支持呢?
let defaultLanguage="zh"
let http = require('http');
function getLanguage(client_langs){
let finalLanguage=defaultLanguage
try{
if(client_langs){
//排序获取语言顺序
client_langs=client_langs.split(',').map(l=>{
let [name,q] = l.split(';');
q = q?Number(q.split('=')[1]):1
return {name,q}
}).sort((a,b)=>b.q-a.q);
//匹配服务器有的语言,并返回
for(let i = 0 ;i <languages.length;i++){
let name= languages[i].name;
if(languages[name]){
finalLanguage=name;
break;
}
}
}
}catch(e){}
return languages[finalLanguage]
}
http.createServer(function (req,res) {
//获取客户端的语言
let client_langs = req.headers['Accept-Language'];
let lan=getLanguage(client_langs)
//将语言打印到客户端
res.end(`<p>${lan.title}</p><p>${lan.content}</p>`)
}).listen(3000);

防盗链

这个技术用的最多的应该就是对于图片的限制,只有本域名可以获取到,其他域名想都不要想。

server client
在某网站上请求了一张图片
通过RefererReferered发现此网站域名不在我方白名单内
此图片不提供给某网站
此时po上了一张万用土
支持正版请上我们网站

实现原理,此处我用iframe来做例子,其实原理很简单就是对比来源,要么和请求资源一致要么在白名单内,不然就拒绝。当然如果没有来源的情况下就直接放行,万一人家是单独打开的呢,不是盗链:

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
let http =  require('http');
let fs = require('fs');
let url = require('url');
let path = require('path');
// 设置白名单
let whiteList = ['localhost:3000'];
http.createServer(function (req,res) {
//获取请求地址
let { pathname } = url.parse(req.url);
// 获取物理地址
let realPath = path.join(__dirname,pathname);
// 获取文件状态
fs.stat(realPath,function(err,statObj) {
if(err){
res.statusCode = 404;
res.end();
}else{
// 重点来了
let Referer = req.headers['Referer'] || req.headers['referred'];
//如果有来源
if(Referer){
//获取双方域名
let current = req.headers['host']
Referer = url.parse(Referer).host
console.log(current,Referer)
//如果域名相同活在白名单中,放行!
if (current === Referer || whiteList.includes(Referer)){
fs.createReadStream(realPath).pipe(res);
}else{
//不放行,此乃盗链!给你个眼神自行体会
fs.createReadStream(path.join(__dirname,'files/2.html')).pipe(res);
}
}else{
//没有来源,也放行。万一是单独打开的呢~
fs.createReadStream(realPath).pipe(res);
}
}
})
}).listen(3000);

gzip

现代浏览器很高级,已经可以接受压缩包了。佩服佩服。那么该如何传输压缩的网页呢?

server client
向server扔过去了Accept-Encoding
大概结构是这样的gzip, deflate, br
get到了对方的用意,开始配置压缩
如果支持压缩,先设置个头部Content-Encoding
有很多种压缩方式,按照server优先支持的匹配
在线压缩网页,成功后返回client
欢欢喜喜省了流量,而且不影响体验

附赠建议代码,大家测试的时候,别忘了创建测试的html文件

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 http = require('http');
//用于压缩文件所需的库
let fs = require('fs');
let path = require('path');
//压缩的库
let zlib = require('zlib');
http.createServer(function (req,res) {
//获取客户端接受的压缩方式
let rule = req.headers['Accept-Encoding'];
// 创建原文件可读流
let originStream=fs.createReadStream(path.join(__dirname, '1.html'));
if(rule){
// 啊啊啊!正则是个坎,我怕我是跨不过去了。
if(rule.match(/\bgzip\b/)){
//如果支持压缩!一定要设置头部!
res.setHeader('Content-Encoding','gzip');
originStream=originStream.pipe(zlib.createGzip())
} else if (rule.match(/\bdeflate\b/)){
res.setHeader('Content-Encoding', 'deflate');
originStream=originStream.pipe(zlib.createDeflate())
}
}
// 输出处理后的可读流
originStream.pipe(res)
}).listen(3000);

中级操作

初级操作大多只需要靠配置HEADER即可以实现,中级我们当然要难一点,大多需要client和server打配合。

  • client给server发送内容(Content-TypeContent-Length)
  • client从server获取内容(RangeContent-Range)
  • client爬虫,抓取网页

client给server发送内容

server client
给你了一串数据,你给处理下
没头没脑,谁知道你要做什么,请设置好HEADER
好吧,告诉你Content-TypeContent-Length
可以可以,数据的内容类型是长度是很必要的
把数据传给你了,你看一下
收到~监听收到的数据是一组Buffer
接受完毕,合并Buffer
根据Content-Type对数据进行处理
格式化数据,end

Server代码

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
let http = require('http');
let server = http.createServer();
let arr=[]
server.on('request', (req, res)=>{
req.on('data',function (data) {
//把获取到的Buffer数据都放入熟组
arr.push(data);
});
req.on('end',function() {
// 请求结束了,好了可以开始处理断断续续收到的Buffer了
// 合并buffer
let r = Buffer.concat(arr).toString();
if (req.headers['content-type'] === 'x-www-form-urlencoded'){
let querystring = require('querystring');
r = querystring.parse(r); // a=1&b=2然后格式化
console.log("querystring",r);
} else if (req.headers['content-type'] === 'application/json'){
//听说是JSON格式的
console.log("json",JSON.parse(r));
} else{
//没有格式?那原来是啥就是啥吧。
console.log("no type",r);
}
arr=[]
res.end('结束了!');
});
})
server.listen(3000,()=>{
console.log(`server start`);
});

Client代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 设置请求地址的配置
let opts = {
host:'localhost',
port:3000,
path:'/',
// 头部设置很重要,头部设置很重要,头部设置很重要
headers:{
'Content-Type':'x-www-form-urlencoded',
//长度超过3就没有人理你了
"Content-Length":7
}
}
let http = require('http');
let client = http.request(opts,function (res) {
res.on('data',function (data) {
console.log(data);
})
});
client.end("a=1&b=2");

client从server获取部分内容

server client
我想要资源的部分内容
可以啊,告诉我范围
我放在HEADER中的Range了,bytes=0-3
Content-Range:bytes 0-3/7,请接受,此文件一共8字节,前3字节已经给你了 好的,那么把接下来的给我吧,bytes=4-7
给你给你都给你 end

大家都发现了吧,这样的range获取数据,完全是断点续传的简陋版啊!不过这边有一个点容易犯错就是文件大小的计算,因为文件字节的位置是按照0开始算,所以range的全范围都是0~size-1/size-1,大家注意下。

server 端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let http = require('http');
let fs = require('fs');
let path = require('path');
// 当前要下载的文件的大小
let size = fs.statSync(path.join(__dirname, 'my.txt')).size;
let server = http.createServer(function (req, res) {
let range = req.headers['range']; //获取client请求访问的部分内容
if (range) {
let [, start, end] = range.match(/(\d*)-(\d*)/);
start = start ? Number(start) : 0;
end = end ? Number(end) : size - 1; // 10个字节 size 10 (0-9)
console.log(`bytes ${start}-${end}/${size - 1}`)
res.setHeader('Content-Range', `bytes ${start}-${end}/${size - 1}`);
fs.createReadStream(path.join(__dirname, 'my.txt'), { start, end }).pipe(res);
} else {
// 会把文件的内容写给客户端
fs.createReadStream(path.join(__dirname, 'my.txt')).pipe(res);
}
});
server.listen(3000);

client端

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
let opts = {
host:'localhost',
port:3000,
headers:{}
}
let http = require('http');
let start = 0;
let fs = require('fs');
function download() {
//分流下载,部分下载
opts.headers.Range = `bytes=${start}-${start+3}`;
start+=4;
let client = http.request(opts,function (res) {
let total = res.headers['content-range'].split('/')[1];
res.on('data',function (data) {
fs.appendFileSync('./download.txt',data);
});
res.on('end',function () {
//结束之后,1s之后再下载
setTimeout(() => {
console.log(start,total)
if (start <= total)
download();
}, 1000);
})
});
client.end();
}
download()

client抓取网页内容,简易爬虫

这一块的操作其实很简单,只要建一个请求获取到网页就可以了。
难点在于:如何将有效的信息剥离网页,过滤掉无用信息。
我这里抓去了百度的娱乐版,百度还算良心,是utf8的,不然就要乱码了。

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
let http = require('http');
let opts = {
host:'news.baidu.com',
path:'/ent'
}
//创建一个请求,获取网站内容
let client = http.request(opts,function (r) {
let arr= [];
//资源不可能一次下载完成,因此每次获取到数据都要push到arr中
r.on('data',function (data) {
arr.push(data);
});
r.on('end',function() {
//合并资源
let result = Buffer.concat(arr).toString();
//对资源进行处理,可以是变成我这样的对象,之后不管做什么处理都很方便
let content = result.match(/<ul class="ulist mix-ulist">(?:[\s\S]*?)<\/ul>/img).toString().match(/<li>(?:[\s\S]*?)<\/li>/img);
content=content.map((c)=>{
let href=/<a href="(?:[\S]*?)"/img.exec(c)
let title=/">(?:[\s\S]*?)<\/a>/img.exec(c)
return {
href:href[0].replace(/"/img,"").replace("<a href=",""),
title:title[0].replace(/">/img,"").replace("</a>","")
}
})
console.log(JSON.stringify(content))
arr= [];
})
});
client.end();

扒一扒PROMISE的原理,大家不要怕!

在前端的日常工作中,回调函数(callback)应该是见怪不怪了,但是当回调函数遇上了异步(async),这就令人发指了。那么异步是什么意思呢,简单地说就是不等你执行完,就先执行下方的代码了。

举个🌰:

我们最常用的异步操作应该是ajax了(想当初我第一次用ajax的时候,简直就是灾难。明明资源加载成功了,怎么就是没有调到资源中的数据呢?真令人头大啊。),只能等待加载完毕,再执行相关操作才能成功。因此我们看到的代码应该都是这样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
@param callback 回调函数
*/
function getData(url,callback){
$.ajax({
url:url,
success:function(result){
callback(result);
}
});
}
//假设我有好多个ajax,每个ajax都需要上一个ajax的支持,于是……地狱出现了……
getData(url1,function(res){
getData(url2,function(res){
getData(url3,function(res){
//终于可以干正事了
})
})
})

朋友们,回调地狱(callback Hell)了解下。

于是promise出现了,他的出现就是解决了回调地狱!他对异步的函数进行了封装,把回调变成了链式调用。

举个🌰:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function getData(url){
return new Promise((resolve,reject)=>{
$.ajax({
url:url,
success:function(result){
resolve(result);
},
error:function(error){
reject(error);
}
});
})
}
getData(url1).then(function(res){
return getData(url2)
}).then(function(res){
return getData(url3)
}).then(function(res){
//干正事啦!
})

确实。简洁了不少,至少不会被里三层外三层的括号弄晕。

但是当初我听到promise的时候,我内心是拒绝的。虽然内心拒绝,但是该来的还是要来的,该学的还是要学的,毕竟时代在进步,与时俱进还是很必要的!那么这个promise是怎么实现的呢???

实现一:状态

小伙伴们,这里promise可不是男女约会中浪漫的台词 ”I promise XXX“ ,而是一种规范,点击此处获取规范。不过这里的promise和现实生活中的promise一样,都有实现(fulfilled),拒绝(rejected)和等待(pending)这三种状态。

举个🌰:

假定 Mary 和 Mike 是一对情侣,半年前,Mike 向 Mary 承诺(promise)半年内完成他们的婚礼,但是直到现在 Mike 也没有做出行动,因此 Mary 表示她不会一直等待(pending)下去,于是他们分手了,那么这个承诺(promise)就是作废了(rejected)。如果这半年内 Mike 和 Mary 结了婚,那么现在 Mike 应该已经实现(fulfilled)了他对 Mary 的承诺(promise)。

所以说,所有的promise都有一个结果状态——实现(fulfilled)或者拒绝(rejected),而结果出来之前的状态就是等待(pending)。

p1.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
//p1.js
function Promise(executor){
let _=this;
_.value=undefined;
_.reason=undefined;
_.state="pending"//大家一开始都是一样,等着吧
function resolve(value){
_.value=value//实现之后的感言
_.state="fulfilled"//实现啦!
}
function reject(reason){
_.reason=reason //给我一个被拒绝的理由
_.state="rejected" //被拒绝了!
}
executor(resolve,reject)
}

//e.g
let Iagree=new Promise((resolve,reject)=>{
resolve("我开心就同意了");//
})
let Idisagree=new Promise((resolve,reject)=>{
reject("我不开心就拒绝了");
})
let noResult=new Promise((resolve,reject)=>{
})
console.log(Iagree.state,Idisagree.state,noResult.state)

实现二:添加then函数

不过我只知道一个状态有何用?我还要进行下一步哒!我们需要一个

1
2

[p2.js](https://github.com/nanaSun/promise/blob/master/p2.js)

//p2.js
Promise.prototype.then=function(onFulfilled, onRejected){
let =this;
if(
.state==”pending”){}
if(.state==”fulfilled”){
onFulfilled(
.value)
}
if(.state==”rejected”){
onRejected(
.reason)
}
}
//e.g
let Iagree=new Promise((resolve,reject)=>{
resolve(“我开心就同意了”);//强行完成(fullfilled)
})
Iagree.then((data)=>{
console.log(Iagree.state)
},(e)=>{
console.log(e)
})

1
2
3
4
5
6
7
8

### 实现三:实现异步执行

不过这个都是同时进行,不是异步的。我们来瞅一眼异步~

这个时候我们需要把回调函数丢到resolve或者reject中,但是如果我们的后续方法很多呢?then好多次怎么办!将回调丢到的队列中,到时候Foreach一下逐个执行。

[p3.js](https://github.com/nanaSun/promise/blob/master/p3.js)

//p3.js
function Promise(executor){
//….
_.resolveCallbacks=[];//callbacks在pending中添加,fullfilled中执行
_.rejectCallbacks=[];//callbacks在pending中添加,rejected中执行
function resolve(value){
//….
_.resolveCallbacks.forEach((fn)=>fn())
}
function reject(reason){
//….
_.rejectCallbacks.forEach((fn)=>fn())
}
//….
}
Promise.prototype.then=function(onFulfilled, onRejected){
let =this;
if(
.state==”pending”){
//把回调方法塞进队列中
.resolveCallbacks.push(()=>{
onFulfilled(
.value)
})
.rejectCallbacks.push(()=>{
onRejected(
.reason)
})
}
//….
}
//e.g
let Iagree=new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(“我开心就同意了”);
},1000)
})

//为了防止多次then,所以回调方法需要丢入队列中,防止方法被覆盖。
Iagree.then((data)=>{
console.log(Iagree.state)
},(e)=>{
console.log(e)
})
Iagree.then((data)=>{
console.log(Iagree.state+1)
},(e)=>{
console.log(e)
})

1
2
3
4

### 实现四:实现链式调用

那么问题来了,如果我直接then,可不可以?像这这样:

Iagree.then((data)=>{

}).then((data)=>{

}).then((data)=>{

})

1
2
3
4
5

如果想要这样写,那么上一步的```then```必须返回一个promise对象才可以,不然哪里变出一个```then```方法。因此我们需要在```then```中```new```一个新的promise,用于下一个链式调用的```then```。


[p4.js](https://github.com/nanaSun/promise/blob/master/p4.js)

//p4.js
function resolvePromise(promise,x,resolve,reject){
//如果x可能是一个promise
if(x!==null&&(typeof x===”object”||typeof x===”function”)){
let then=x.then;
//如果x是一个promise,因为promise都要有then函数的
if(typeof then === “function”){
//y表示x这个promise的值
then.call(x,y=>{
//继续遍历,直至返回值不是promise
resolvePromise(promise,y,resolve,reject)
},err=>{
reject(err)
})
}else{
//如果x是个普通对象,直接运行
resolve(x)
}
}else{
//如果x不是一个promise,也就是x是一个常量,直接运行
resolve(x)
}
}
Promise.prototype.then=function(onFulfilled, onRejected){
let =this;
let promise2;
//将当前promise的值传递到下一次then的调用中
function resolveFunction(promise,resolve,reject){
let x=onFulfilled(
.value)
resolvePromise(promise,x,resolve,reject)
}
function rejectFunction(promise,resolve,reject){
let x=onRejected(.reason)
resolvePromise(promise,x,resolve,reject)
}
promise2=new Promise((resolve,reject)=>{
if(
.state==”pending”){
//把回调方法塞进队列中
_.resolveCallbacks.push(()=>{
resolveFunction(promise2,resolve,reject)
})
.rejectCallbacks.push(()=>{
rejectFunction(promise2,resolve,reject)
})
}
if(
.state==”fulfilled”){
resolveFunction(promise2,resolve,reject)
}
if(_.state==”rejected”){
rejectFunction(promise2,resolve,reject)
}
})
return promise2
}
//e.g
let Iagree=new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(“我开心就同意了”);
},1000)
})

//为了防止多次then,所以回调方法需要丢入队列中,防止方法被覆盖。
Iagree.then((data)=>{
console.log(data)
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(“看心情干活”);
},1000)
})
}).then((data)=>{
console.log(“前方返回一个promise:”+data)
return data+”,我是一个常量”
}).then((data)=>{
console.log(“常量返回:”+data)
}).then((data)=>{
console.log(“前方无法返回:”+data)
})

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

这样我们就可以愉快地用链式调用promise了,想想就美滋滋。
不过以上只是简单粗暴的实现promise的方式,只是一个原理,还有promise的一些规范需要完善[点击此处获取规范](https://promisesaplus.com/)。

### 符合promisesA+的规范

总结几点

* 该`try{}catch(){}`的地方都标记上,宁可错杀不放过。
* onFulfilled和onRejected的方法放入`setTimeout`之中,为了让他们变成“宏任务(macro-task)”。(应该是出于性能的考虑,之后再研究。)
* 然后加一个`Promise.defer = Promise.deferred = function(){}`方法,防止篡改。
* 接着导出promise,`module.exports=Promise`。
* 最后运行一波`promises-aplus-tests.cmd 你的promise.js`,然后一行行地检查你的代码,等到全部变绿(passing),恭喜你成功攻克promise!!

[p5.js](https://github.com/nanaSun/promise/blob/master/p5.js)

//参考p5.js
`