CSS in JS是一种解决css问题想法的集合,而不是一个指定的库。从CSS in JS的字面意思可以看出,它是将css样式写在JavaScript文件中,而不需要独立出.css、.less之类的文件。将css放在js中使我们更方便的使用js的变量、模块化、tree-shaking。还解决了css中的一些问题,譬如:更方便解决基于状态的样式,更容易追溯依赖关系,生成唯一的选择器来锁定作用域。尽管CSS in JS不是一个很新的技术,但国内的普及程度并不高。由于Vue和Angular都有属于他们自己的一套定义样式的方案,React本身也没有管用户怎样定义组件的样式[1],所以CSS in JS在React社区的热度比较高。
目前为止实现CSS in JS的第三方库有很多:(http://michelebertoli.github.io/css-in-js/)。像JSS[2]、styled-components[3]等。在这里我们就不展开赘述了(相关链接已放在下方),这篇文章的重点是JS in CSS。
在上面我们提到CSS in JS就是把CSS写在JavaScript中,那么JS in CSS我们可以推断出就是可以在CSS中使用JavaScript脚本,如下所示。可以在CSS中编写Paint API的功能。还可以访问:ctx,geom。甚至我们还可以编写自己的css自定义属性等。这些功能的实现都基于CSS Houdini[4]。
.el {
--color: cyan;
--multiplier: 0.24;
--pad: 30;
--slant: 20;
--background-canvas: (ctx, geom) => {
let multiplier = var(--multiplier);
let c = `var(--color)`;
let pad = var(--pad);
let slant = var(--slant);
ctx.moveTo(0, 0);
ctx.lineTo(pad + (geom.width - slant - pad) * multiplier, 0);
ctx.lineTo(pad + (geom.width - slant - pad) * multiplier + slant, geom.height);
ctx.lineTo(0, geom.height);
ctx.fillStyle = c;
ctx.fill();
};
background: paint(background-canvas);
transition: --multiplier .4s;
}
.el:hover {
--multiplier: 1;
}
在如今的Web开发中,JavaScript几乎占据了项目代码的大部分。我们可以在项目开发中使用ES 2020、ES2021、甚至提案中的新特性(如:Decorator[5]),即使浏览器尚未支持,也可以编写Polyfill或使用Babel之类的工具进行转译,让我们可以将最新的特性应用到生产环境中(如下图所示)。
JavaScript标准制定流程.png
而CSS就不同了,除了制定CSS标准规范所需的时间外,各家浏览器的版本、实战进度差异更是旷日持久(如下图所示),最多利用PostCSS、Sass等工具來帮我们转译出浏览器能接受的CSS。开发者们能操作的就是通过JS去控制DOM与CSSOM来影响页面的变化,但是对于接下來的Layout、Paint与Composite就几乎没有控制权了。为了解决上述问题,为了让CSS的魔力不在受到浏览器的限制,Houdini就此诞生。
CSS 标准制定流程.png
我们上文中提到JavaScript中进入提案中的特性我们可以编写Polyfill,只需要很短的时间就可以讲新特性投入到生产环境中。这时,脑海中闪现出的第一个想法就是CSS Polyfill,只要CSS的Polyfill 足够强大,CSS或许也能有JavaScript一样的发展速度,令人可悲的是编写CSS Polyfill异常的困难,并且大多数情况下无法在不破坏性能的情况下进行。这是因为JavaScript是一门动态脚本语言[6]。它带来了极强的扩展性,正是因为这样,我们可以很轻松使用JavaScript做出JavaScript的Polyfill。但是CSS不是动态的,在某些场景下,我们可以在编译时将一种形式的CSS的转换成另一种(如PostCSS[7])。如果你的Polyfill依赖于DOM结构或者某一个元素的布局、定位等,那么我们的Polyfill就无法编译时执行,而需要在浏览器中运行了。不幸的是,在浏览器中实现这种方案非常不容易。
页面渲染流程.png
如上图所示,是从浏览器获取到HTML到渲染在屏幕上的全过程,我们可以看到只有带颜色(粉色、蓝色)的部分是JavaScript可以控制的环节。首先我们根本无法控制浏览器解析HTML与CSS并将其转化为DOM与CSSOM的过程,以及Cascade,Layout,Paint,Composite我们也无能为力。整个过程中我们唯一完全可控制的就是DOM,另外CSSOM部分可控。
CSS Houdini草案中提到,这种程度的暴露是不确定的、兼容性不稳定的以及缺乏对关键特性的支持的。比如,在浏览器中的 CSSOM 是不会告诉我们它是如何处理跨域的样式表,而且对于浏览器无法解析的 CSS 语句它的处理方式就是不解析了,也就是说——如果我们要用 CSS polyfill让浏览器去支持它尚且不支持的属性,那就不能在 CSSOM 这个环节做,我们只能遍历一遍DOM,找到 <style> 或 <link rel="stylesheet"> 标签,获取其中的 CSS 样式、解析、重写,最后再加回 DOM 树中。令人尴尬的是,这样DOM树全部刷新了,会导致页面的重新渲染(如下如所示)。
即便如此,有的人可能会说:“除了这种方法,我们也别无选择,更何况对网站的性能也不会造成很大的影响”。那么对于部分网站是这样的。但如果我们的Polyfill是需要对可交互的页面呢?例如scroll,resize,mousemove,keyup等等,这些事件随时会被触发,那么意味着随时都会导致页面的重新渲染,交互不会像原本那样丝滑,甚至导致页面崩溃,对用户的体验也极其不好。
综上所述,如果我们想让浏览器解析它不认识的样式(低版本浏览器使用grid布局),然而渲染流程我们无法介入,我们也只能通过手动更新DOM的方式,这样会带来很多问题,Houdini的出现正是致力于解决他们。
Houdini是一组底层API,它公开了CSS引擎的各个部分,如下图所示展示了每个环节对应的新API(灰色部分各大浏览器还未实现),从而使开发人员能够通过加入浏览器渲染引擎的样式和布局过程来扩展CSS。Houdini是一群来自Mozilla,Apple,Opera,Microsoft,HP,Intel和Google的工程师组成的工作小组设计而成的。它们使开发者可以直接访问CSS对象模型(CSSOM),使开发人员可以编写浏览器可以解析为CSS的代码,从而创建新的CSS功能,而无需等待它们在浏览器中本地实现。
CSS Houdini-API
尽管当前已经有了CSS变量,可以让开发者控制属性值,但是无法约束类型或者更严格的定义,CSS Houdini新的API,我们可以扩展css的变量,我们可以定义CSS变量的类型,初始值,继承。它是css变量更强大灵活。
CSS变量现状:
.dom {
--my-color: green;
--my-color: url('not-a-color'); // 它并不知道当前的变量类型
color: var(--my-color);
}
Houdini提供了两种自定义属性的注册方式,分别是在js和css中。
CSS.registerProperty({
name: '--my-prop', // String 自定义属性名
syntax: '<color>', // String 如何去解析当前的属性,即属性类型,默认 *
inherits: false, // Boolean 如果是true,子节点将会继承
initialValue: '#c0ffee', // String 属性点初始值
});
我们还可以在css中注册,也可以达到上面的效果
@property --my-prop {
syntax: '<color>';
inherits: false;
initial-value: #c0ffee;
}
这个API中最令人振奋人心的功能是自定义属性上添加动画,像这样:transition: --multiplier 0.4s;,这个功能我们在前面介绍什么是js in css那个demo[8]用使用过。我们还可以使用+使syntax属性支持一个或多个类型,也可以使用|来分割。更多syntax属性值:
属性值描述<length>长度值<number>数字<percentage>百分比<length-percentage>长度或百分比,calc将长度和百分比组成的表达式<color>颜色<image>图像<url>网址<integer>整数<angle>角度<time>时间<resolution>分辨率<transform-list>转换函数<custom-ident>ident
Worklets是渲染引擎的扩展,从概念上来讲它类似于Web Workers[9],但有几个重要的区别:
Worklet是一个JavaScript模块,通过调用worklet的addModule方法(它是个Promise)来添加。比如registerLayout,registerPaint, registerAnimator 我们都需要放在Worklet中
//加载单个
await demoWorklet.addModule('path/to/script.js');
// 一次性加载多个worklet
Promise.all([
demoWorklet1.addModule('script1.js'),
demoWorklet2.addModule('script2.js'),
]).then(results => {});
registerDemoWorklet('name', class {
// 每个Worklet可以定义要使用的不同函数
// 他们将由渲染引擎在需要时调用
process(arg) {
return !arg;
}
});
Worklets的生命周期
Worklets lifecycle
Typed OM是对现有的CSSOM的扩展,并实现 Parsing API 和 Properties & Values API相关的特性。它将css值转化为有意义类型的JavaScript的对象,而不是像现在的字符串。如果我们尝试将字符串类型的值转化为有意义的类型并返回可能会有很大的性能开销,因此这个API可以让我们更高效的使用CSS的值。
现在读取CSS值增加了新的基类CSSStyleValue,他有许多的子类可以更加精准的描述css值的类型:
子类描述CSSKeywordValueCSS关键字和其他标识符(如inherit或grid)CSSPositionValue位置信息 (x,y)CSSImageValue表示图像的值属性的对象CSSUnitValue表示为具有单个单位的单个值(例如50px),也可以表示为没有单位的单个值或百分比CSSMathValue比较复杂的数值,比如有calc,min和max。这包括子类 CSSMathSum, CSSMathProduct, CSSMathMin,CSSMathMax, CSSMathNegate 和 CSSMathInvertCSSTransformValue由CSS transforms组成的CSSTransformComponent列表,其中包括CSSTranslate, CSSRotate, CSSScale, CSSSkew, CSSSkewX, CSSSkewY, CSSPerspective 和 CSSMatrixComponent
使用Typed OM主要有两种方法:
使用attributeStyleMap设置并获取
myElement.attributeStyleMap.set('font-size', CSS.em(2));
myElement.attributeStyleMap.get('font-size'); // CSSUnitValue { value: 2, unit: 'em' }
myElement.attributeStyleMap.set('opacity', CSS.number(.5));
myElement.attributeStyleMap.get('opacity'); // CSSUnitValue { value: 0.5, unit: 'number' };
在线demo[10]
使用computedStyleMap
.foo {
transform: translateX(1em) rotate(50deg) skewX(10deg);
vertical-align: baseline;
width: calc(100% - 3em);
}
const cs = document.querySelector('.foo').computedStyleMap();
cs.get('vertical-align');
// CSSKeywordValue {
// value: 'baseline',
// }
cs.get('width');
// CSSMathSum {
// operator: 'sum',
// length: 2,
// values: CSSNumericArray {
// 0: CSSUnitValue { value: -90, unit: 'px' },
// 1: CSSUnitValue { value: 100, unit: 'percent' },
// },
// }
cs.get('transform');
// CSSTransformValue {
// is2d: true,
// length: 3,
// 0: CSSTranslate {
// is2d: true,
// x: CSSUnitValue { value: 20, unit: 'px' },
// y: CSSUnitValue { value: 0, unit: 'px' },
// z: CSSUnitValue { value: 0, unit: 'px' },
// },
// 1: CSSRotate {...},
// 2: CSSSkewX {...},
// }
开发者可以通过这个API实现自己的布局算法,我们可以像原生css一样使用我们自定义的布局(像display:flex, display:table)。在Masonry layout library[11] 上我们可以看到开发者们是有多想实现各种各样的复杂布局,其中一些布局光靠 CSS 是不行的。虽然这些布局会让人耳目一新印象深刻,但是它们的页面性能往往都很差,在一些低端设备上性能问题犹为明显。
CSS Layout API 暴露了一个registerLayout方法给开发者,接收一个布局名(layout name)作为后面在 CSS中使用的属性值,还有一个包含有这个布局逻辑的JavaScript类。
my-div {
display: layout(my-layout);
}
// layout-worklet.js
registerLayout('my-layout', class {
static get inputProperties() { return ['--foo']; }
static get childrenInputProperties() { return ['--bar']; }
async intrinsicSizes(children, edges, styleMap) {}
async layout(children, edges, constraints, styleMap) {}
});
await CSS.layoutWorklet.addModule('layout-worklet.js');
目前浏览器大部分还不支持
我们可以在CSS background-image中使用它,我们可以使用Canvas 2d上下文,根据元素的大小控制图像,还可以使用自定义属性。
await CSS.paintWorklet.addModule('paint-worklet.js');
registerPaint('sample-paint', class {
static get inputProperties() { return ['--foo']; }
static get inputArguments() { return ['<color>']; }
static get contextOptions() { return {alpha: true}; }
paint(ctx, size, props, args) { }
});
这个API让我们可以控制基于用户输入的关键帧动画,并且以非阻塞的方式。还能更改一个 DOM 元素的属性,不过是不会引起渲染引擎重新计算布局或者样式的属性,比如 transform、opacity 或者滚动条位置(scroll offset)。Animation API的使用方式与 Paint API 和Layout API略有不同我们还需要通过new一个WorkletAnimation来注册worklet。
// animation-worklet.js
registerAnimator('sample-animator', class {
constructor(options) {
}
animate(currentTime, effect) {
effect.localTime = currentTime;
}
});
await CSS.animationWorklet.addModule('animation-worklet.js');
// 需要添加动画的元素
const elem = document.querySelector('#my-elem');
const scrollSource = document.scrollingElement;
const timeRange = 1000;
const scrollTimeline = new ScrollTimeline({
scrollSource,
timeRange,
});
const effectKeyframes = new KeyframeEffect(
elem,
// 动画需要绑定的关键帧
[
{transform: 'scale(1)'},
{transform: 'scale(.25)'},
{transform: 'scale(1)'}
],
{
duration: timeRange,
},
);
new WorkletAnimation(
'sample-animator',
effectKeyframes,
scrollTimeline,
{},
).play();
关于此API的更多内容:(https://github.com/w3c/css-houdini-drafts/tree/main/css-animation-worklet-1)
允许开发者自由扩展 CSS 词法分析器。
解析规则:
const background = window.cssParse.rule("background: green");
console.log(background.styleMap.get("background").value) // "green"
const styles = window.cssParse.ruleSet(".foo { background: green; margin: 5px; }");
console.log(styles.length) // 5
console.log(styles[0].styleMap.get("margin-top").value) // 5
console.log(styles[0].styleMap.get("margin-top").type) // "px"
解析CSS:
const style = fetch("style.css")
.then(response => CSS.parseStylesheet(response.body));
style.then(console.log);
它将提供一些方法来测量在屏幕上呈现的文本元素的尺寸,将允许开发者控制文本元素在屏幕上呈现的方式。使用当前功能很难或无法测量这些值,因此该API将使开发者可以更轻松地创建与文本和字体相关的CSS特性。例如:
Is Houdini ready yet
(https://ishoudinireadyyet.com/)
了解到这里,部分开发者可能会说:“我不需要这些花里胡哨的技术,并不能带收益。我只想简简单单的写几个页面,做做普通的Web App,并不想试图干预浏览器的渲染过程从而实现一些实验性或炫酷的功能。”如果这样想的话,我们不妨退一步再去思考。回忆下最近做过的项目,用于实现页面效果所使用到的技术,grid布局方式在考虑兼容老版本浏览器时也不得不放弃。我们想控制浏览器渲染页面的过程并不是仅仅为了炫技,更多的是为了帮助开发者们解决以下两个问题:
几年过后再回眸,当主流浏览器完全支持Houdini的时候。我们可以在浏览器上随心所欲的使用任何CSS属性,并且他们都能完美支持。像今天的grid布局在旧版本浏览器支持的并不友好的这类问题,那时我们只需要安装对应的Polyfill就能解决类似的问题。
前一阵儿被某网站的 JS 反爬流程难住了,至今也没明白它的反扒原理和攻破方法。最终找到了一个自动化脚本工具 autoit 3,用一个笨方法将人手动点击浏览器的动作脚本化,达到网页数据获取目的,拿到网页文件后,再用代码解析,曲线完成任务。
本文将介绍这个自动化的过程,并带编写一个完整的 autoit 3 爬虫脚本,希望对各位读者朋友有所启发。
以国家信息安全漏洞共享平台为例,它在返回数据前发起了两次 512 响应,第三次浏览器带着动态生成的 Cookie 信息才能得到数据。
这次咱们直接从网页入手,操作键盘找到“下一页” 按钮,按下 Enter 键完全请求。通过键盘定位到 “下页” 按钮的过程为:
接着就可以编写自动化脚本了,把刚刚的手动操作翻译成脚本命令:
这个流程,对其他高反扒的信息发布网站,也是适用的。
按照上面的流程,编写 autoit 自动化脚本,创建一个 myspider.au3 文件:
#include <AutoItConstants.au3>
;;切换为英文输入法,保证浏览器输入正常
$hWnd = WinGetHandle("[ACTIVE]");$hWnd 为目标窗口句柄,这里设置的是当前活动窗口
$ret = DllCall("user32.dll", "long", "LoadKeyboardLayout", "str", "08040804", "int", 1 + 0)
DllCall("user32.dll", "ptr", "SendMessage", "hwnd", $hWnd, "int", 0x50, "int", 1, "int", $ret[0])
$url = "https://www.cnvd.org.cn/flaw/list.htm"
spiderData($url)
Func spiderData($url)
;;打开 Chrome 浏览器窗口
$chromePath = "C:\Users\admin\AppData\Local\Google\Chrome\Application\chrome.exe"
Run($chromePath)
;;登录窗口显示
WinWaitActive("[CLASS:Chrome_WidgetWin_1]")
;; 休息2000毫秒
Sleep(2000)
;; 移动窗口
WinMove("[CLASS:Chrome_WidgetWin_1]", "打开新的标签页 - Google Chrome", 0, 0,1200,740,2)
;; 休息500毫秒
Sleep(500)
;;地址栏输入URL 并按下 Enter 键
Send($url)
Sleep(500)
Send("{enter}")
Sleep(3000)
;; 循环爬取需要的页数,测试只爬3页
For $i = 1 To 3 Step 1
;;打开右键另存为按钮: Ctrl+S
send("^s")
Sleep(2000)
WinWait("[CLASS:#32770]","",10)
;;将存储路径设置到另存为组件输入框 Edit1 里
$timeNow = @YEAR & "" & @MON & "" & @MDAY & "" & @HOUR & "" & @MIN
$savePath = "F:\A2021Study\ListData\" &$timeNow & "_page" & $i & ".html"
ControlSetText("另存为","", "Edit1", $savePath)
;;点击确定
ControlClick("另存为","","Button2")
;;再次确定
WinWait("[CLASS:#32770]","",10)
ControlClick("确认另存为","","Button1")
;; 等待保存操作完成
Sleep(3000)
;; 定位到下一页按钮,并触发点击下一页
send("{END}")
Send("+{TAB 15}")
Send("{enter}")
;;点击确定后,等待网页加载完成
Sleep(3000)
Next
;; 整个操作完成,则关闭浏览器
Send("^w")
EndFunc
脚本编写过程中,有几点需要注意:
因为爬虫要作为定时任务运行的,为避免打开太多浏览器窗口,因此需要在脚本结束时关闭浏览器。
数据爬取一般分为列表页和详情页,定位点击每一条详情的过程比较麻烦,所以爬取详情页面的和列表分开,用 Java 代码解析所有详情 URL 后,再由另一个 autoit 脚本去获取详情页面,这个流程大家可以自己写一下,这里就不详细介绍了。
最后再汇总下整个爬取的流程:
第一步,执行爬取列表的 autoit 脚本,得到列表页面 html;
第二步,解析列表页 html ,得到所有详情页面的 URL ,写入到文件中;
第三步,执行爬取详情页面的 autoit 脚本,它遍历第二步的目标 URL ,得到详情页 html ;
第四步,解析详情页 html 文件,得到详情数据。
总控流程、第二步和第四步的解析都用 Java 代码完成,用 Runtime.getRuntime().exec("cmd /c E:\A2021Study\Autoit3\myspider.au3") 调用脚本,文件路径是反斜杠。
这个方法虽然有点笨,但完全是人工操作浏览器,能够对抗反爬虫策略,感兴趣的朋友可以执行下本文的脚本试试。
autoit 还是蛮有意思的,语法也很简单,DirCreate 创建文件,iniread 读取配置项,一行代码顶 Java 几十行,不得不承认 Java 操作文件才是最麻烦的哇!
HTTP车是由蒂姆·伯纳斯-李( TimBerners—Lee )于1989年在欧洲核子研究组织( CERN )所发起
其中最著名的是 1999 年 6 月公布的 RFC 2616 ,定义了 HTTP 协议中现今广泛使用的一个版本—— HTTP 1.1
全称:超文本传输协议( HyperText Transfer Protocol )
概念: HTTP 是一种能够获取像 HTML 、图片等网络资源的通讯协议( protocol )。它是在 web 上进行数据交换的基础,是一种 client-server 协议
HTTP ——因特网的多媒体信使 ——《HTTP权威指南》。 HTTP 在因特网的角色:充当一个信使的角色,干的就是一个跑腿的活,在客户端和服务端之间传递信息,但我们又不能缺少它。 HTTP 协议是应用层的协议,是与前端开发最息息相关的协议。平时我们遇到的 HTTP 请求、 HTTP 缓存、 Cookies 、跨域等其实都跟 HTTP 息息相关
也就是说, HTTP 依赖于面向连接的 TCP 进行消息传递,但连接并不是必须的。只需要它是可靠的,或不丢失消息的(至少返回错误)。
HTTP/1.0 默认为每一对 HTTP 请求/响应都打开一个单独的 TCP 连接。当需要连续发起多个请求时,这种模式比多个请求共享同一个 TCP 链接更低效。为此, HTTP 1.1 持久连接的概念,底层 TCP 连接可以通过 connection 头部实现。但 HTTP 1.1 在连接上也是不完美的,后面我们会提到。
HTTP 的组件系统包括客户端、 web 服务器和代理
浏览器,特殊比如是工程师使用的程序,以及 Web 开发人员调试应用程序
由 Web Server 来服务并提供客户端所请求的文档。每一个发送到服务器的请求,都会被服务器处理并返回一个消息,也就是 response
在浏览器和服务器之间,有很多计算机和其他设备转发了 HTTP 消息。它们可能出现在传输层、网络层和物理层上,对于 HTTP 应用层而言就是透明的
有如下的一些作用
HTTP 有两种类型的消息:
HTTP 消息由采用 ASCII 编码的多行文本构成的。在 HTTP/1.1 以及更早的版本中,这些消息通过连接公开的发送。在 HTTP2.0 中,消息被分到了多个 HTTP 帧中。通过配置文件(用于代理服务器或者服务器), API (用于浏览器)或者其他接口提供 HTTP 消息
HTTP 请求和响应都包括起始行( start line )、请求头( HTTP Headers )、空行( empty line )以及 body 部分,如下图所示:
下面详细说下请求 Path ,请求路径( Path )有以下几种:
1)一个绝对路径,末尾跟上一个 ' ? ' 和查询字符串。这是最常见的形式,称为 原始形式 ( origin form ),被 GET , POST , HEAD 和 OPTIONS 方法所使用
POST / HTTP/1.1
GET /background.png HTTP/1.0
HEAD /test.html?query=alibaba HTTP/1.1
OPTIONS /anypage.html HTTP/1.0
复制代码
2)一个完整的 URL 。主要在使用 GET 方法连接到代理的时候使用
GET http://developer.mozilla.org/en-US/docs/Web/HTTP/Messages HTTP/1.1
复制代码
3)由域名和可选端口(以':'为前缀)组成的 URL 的 authority component ,称为 authority form 。仅在使用 CONNECT 建立 HTTP 隧道时才使用
CONNECT developer.mozilla.org:80 HTTP/1.1
复制代码
4)星号形式 ( asterisk form ),一个简单的星号('*'),配合 OPTIONS 方法使用,代表整个服务器。
OPTIONS * HTTP/1.1
复制代码
请求 Body 部分: 有些请求将数据发送到服务器以便更新数据:常见的的情况是 POST 请求(包含 HTML 表单数据)。请求报文的 Body 一般为两类。一类是通过 Content-Type 和 Content-Length 定义的单文件 body 。另外一类是由多 Body 组成,通常是和 HTML Form 联系在一起的。两者的不同表现在于 Content-Type 的值。
1) Content-Type —— application/x-www-form-urlencoded 对于 application/x-www-form-urlencoded 格式的表单内容,有以下特点:
I.其中的数据会被编码成以&分隔的键值对
II.字符以URL编码方式编码。
// 转换过程: {a: 1, b: 2} -> a=1&b=2 -> 如下(最终形式)
"a%3D1%26b%3D2"
复制代码
2) Content-Type —— multipart/form-data
请求头中的 Content-Type 字段会包含 boundary ,且 boundary 的值有浏览器默认指定。例: Content-Type: multipart/form-data;boundary=----WebkitFormBoundaryRRJKeWfHPGrS4LKe 。
数据会分为多个部分,每两个部分之间通过分隔符来分隔,每部分表述均有 HTTP 头部描述子包体,如 Content-Type ,在最后的分隔符会加上--表示结束。
Content-Disposition: form-data;name="data1";
Content-Type: text/plain
data1
----WebkitFormBoundaryRRJKeWfHPGrS4LKe
Content-Disposition: form-data;name="data2";
Content-Type: text/plain
data2
----WebkitFormBoundaryRRJKeWfHPGrS4LKe--
复制代码
响应 Body 部分:
1)由已知长度的单个文件组成。该类型 body 有两个 header 定义: Content-Type 和 Content-Length
2)由未知长度的单个文件组成,通过将 Transfer-Encoding 设置为 chunked 来使用 chunks 编码。
关于 Content-Length 在下面 HTTP 1.0 中会提到,这个是 HTTP 1.0 中新增的非常重要的头部。
安全方法: HTTP 定义了一组被称为安全方法的方法。 GET 方法和 HEAD 方法都被认为是安全的,这意味着 GET 方法和 HEAD 方法都不会产生什么动作 —— HTTP 请求不会再服务端产生什么结果,但这并不意味着什么动作都没发生,其实这更多的是 web 开发者决定的
首先要了解下副作用和幂等的概念,副作用指的是对服务器端资源做修改。幂等指发送 M 和 N 次请求(两者不相同且都大于 1),服务器上资源的状态一致。应用场景上,get是无副作用的,幂等的。post 主要是有副作用的,不幂等的情况
技术上有以下的区分:
HTTP Headers
1.通用首部( General headers )同时适用于请求和响应消息,但与最终消息主体中传输的数据无关的消息头。如 Date
2.请求首部( Request headers )包含更多有关要获取的资源或客户端本身信息的消息头。如 User-Agent
3.响应首部( Response headers )包含有关响应的补充信息
4.实体首部( Entity headers )含有关实体主体的更多信息,比如主体长( Content-Length )度或其 MIME 类型。如 Accept-Ranges
详细的 Header 见 HTTP Headers 集合
HTTP(HyperText Transfer Protocol) 是万维网( World Wide Web )的基础协议。 Tim Berners-Lee 博士和他的团队在 1989-1991 年间创造出它。【HTTP、网络浏览器、服务器】
在 1991 年发布了 HTTP 0.9 版,在 1996 年发布 1.0 版,1997 年是 1.1 版,1.1 版也是到今天为止传输最广泛的版本。2015 年发布了 2.0 版,其极大的优化了 HTTP/1.1 的性能和安全性,而 2018 年发布的 3.0 版,继续优化 HTTP/2 ,激进地使用 UDP 取代 TCP 协议,目前, HTTP/3 在 2019 年 9 月 26 日 被 Chrome , Firefox ,和 Cloudflare 支持
单行协议,请求由单行指令构成。以唯一可用的方法 GET 开头。后面跟的是目标资源的路径
GET /mypage.html
复制代码
响应:只包括响应文档本身
<HTML>
这是一个非常简单的HTML页面
</HTML>
复制代码
HTML
RFC 1945 提出了 HTTP1.0 , 构建更好可拓展性
媒体类型是一种标准。用来表示文档、文件或者字节流的性质和格式。浏览器通常使用 MIME ( Multipurpose Internet Mail Extensions )类型来确定如何处理 URL ,因此 Web 服务器在响应头中配置正确的 MIME 类型会非常的重要。如果配置不正确,可能会导致网站无法正常的工作。 MIME 的组成结构非常简单;由类型与子类型两个字符串中间用'/'分隔而组成。
HTTP 从 MIME type 取了一部分来标记报文 body 部分的数据类型,这些类型体现在 Content-Type 这个字段,当然这是针对于发送端而言,接收端想要收到特定类型的数据,也可以用 Accept 字段。
这两个字段的取值可以分为下面几类:
- text: text/html, text/plain, text/css 等
- image: image/gif, image/jpeg, image/png 等
- audio/video: audio/mpeg, video/mp4 等
- application: application/json, application/javascript, application/pdf, application/octet-stream
复制代码
同时为了约定请求的数据和响应数据的压缩方式、支持语言、字符集等,还提出了以下的 Header
1.压缩方式:发送端: Content-Encoding (服务端告知客户端,服务器对实体的主体部分的编码方式) 和 接收端: Accept-Encoding (用户代理支持的编码方式),值有 gzip: 当今最流行的压缩格式;deflate: 另外一种著名的压缩格式;br: 一种专门为 HTTP 发明的压缩算法
2.支持语言: Content-Language 和 Accept-Language (用户代理支持的自然语言集)
3.字符集:发送端: Content-Type 中,以 charset 属性指定。接收端: Accept-Charset (用户代理支持的字符集)。
// 发送端
Content-Encoding: gzip
Content-Language: zh-CN, zh, en
Content-Type: text/html; charset=utf-8
// 接收端
Accept-Encoding: gzip
Accept-Language: zh-CN, zh, en
Accept-Charset: charset=utf-8
复制代码
虽然 HTTP1.0 在 HTTP 0.9 的基础上改进了很多,但还是存在这不少的缺点
HTTP/1.0 版的主要缺点是,每个 TCP 连接只能发送一个请求。发送数据完毕,连接就关闭,如果还要请求其他资源,就必须再新建一个连接。 TCP 连接的新建成本很高,因为需要客户端和服务器三次握手,并且开始时发送速率较慢( slow start )。
HTTP 最早期的模型,也是 HTTP/1.0 的默认模型,是短连接。每一个 HTTP 请求都由它自己独立的连接完成;这意味着发起每一个 HTTP 请求之前都会有一次 TCP 握手,而且是连续不断的。
HTTP/1.1 在1997年1月以 RFC 2068 文件发布。
HTTP 1.1 消除了大量歧义内容并引入了多项技术
虚拟主机( virtual hosting )即共享主机( shared web hosting ),可以利用虚拟技术把一台完整的服务器分成若干个主机,因此可以在单一主机上运行多个网站或服务。
举个例子,有一台 ip 地址为 61.135.169.125 的服务器,在这台服务器上部署着谷歌、百度、淘宝的网站。为什么我们访问 https://www.google.com 时,看到的是 Google 的首页而不是百度或者淘宝的首页?原因就是 Host 请求头决定着访问哪个虚拟主机。
2015年, HTTP2.0 面世。 rfc7540
HTTP 2.0 中的帧将 HTTP/1.x 消息分成帧并嵌入到流 ( stream ) 中。数据帧和报头帧分离,这将允许报头压缩。将多个流组合,这是一个被称为多路复用 ( multiplexing ) 的过程,它允许更有效的底层 TCP 连接。
也就是说,流用来承载消息,消息又是有一个或多个帧组成。二进制传输的方式更加提升了传输性能。 每个数据流都以消息的形式发送,而消息又由一个或多个帧组成。 帧是流中的数据单位。
HTTP 帧现在对 Web 开发人员是透明的。在 HTTP/2 中,这是一个在 HTTP/1.1 和底层传输协议之间附加的步骤。 Web 开发人员不需要在其使用的 API 中做任何更改来利用 HTTP 帧;当浏览器和服务器都可用时, HTTP/2 将被打开并使用。
之前我们提到,虽然 HTTP 1.1 有了长连接和管道化的技术,但是还是会存在 队头阻塞。而 HTTP 2.0 就解决了这个问题 HTTP/2 中新的二进制分帧层突破了这些限制,实现了完整的请求和响应复用:客户端和服务器可以将 HTTP 消息分解为互不依赖的帧,然后交错发送,最后再在另一端把它们重新组装起来。
如上图所示,快照捕捉了同一个连接内并行的多个数据流。 客户端正在向服务器传输一个 DATA 帧(数据流 5),与此同时,服务器正向客户端交错发送数据流 1 和数据流 3 的一系列帧。因此,一个连接上同时有三个并行数据流。
将 HTTP 消息分解为独立的帧,交错发送,然后在另一端重新组装是 HTTP 2 最重要的一项增强。事实上,这个机制会在整个网络技术栈中引发一系列连锁反应,从而带来巨大的性能提升,让我们可以: 1.并行交错地发送多个请求,请求之间互不影响。 2.并行交错地发送多个响应,响应之间互不干扰。 3.使用一个连接并行发送多个请求和响应。 4.消除不必要的延迟和提高现有网络容量的利用率,从而减少页面加载时间。 5.不必再为绕过 HTTP/1.x 限制而做很多工作(比如精灵图) ...
连接共享,即每一个 request 都是是用作连接共享机制的。一个 request 对应一个 id ,这样一个连接上可以有多个 request ,每个连接的 request 可以随机的混杂在一起,接收方可以根据 request 的 id 将 request 再归属到各自不同的服务端请求里面。
HTTP 1.1 和 HTTP 2.0 的对比,可以参考这个 网站 demo 演示
HTTP 1.1 演示如下:
HTTP2.0 演示如下:
使用 HTTP/1.1 和 HTTP/2 对于站点和应用来说是透明的。拥有一个最新的服务器和新点的浏览器进行交互就足够了。只有一小部分群体需要做出改变,而且随着陈旧的浏览器和服务器的更新,而不需 Web 开发者做什么,用的人自然就增加了
HTTPS 也是通过 HTTP 协议进行传输信息,但是采用了 TLS 协议进行了加密
对称加密就是两边拥有相同的秘钥,两边都知道如何将密文加密解密。但是因为传输数据都是走的网络,如果将秘钥通过网络的方式传递的话,一旦秘钥被截获就没有加密的意义的
非对称加密
公钥大家都知道,可以用公钥加密数据。但解密数据必须使用私钥,私钥掌握在颁发公钥的一方。首先服务端将公钥发布出去,那么客户端是知道公钥的。然后客户端创建一个秘钥,并使用公钥加密,发送给服务端。服务端接收到密文以后通过私钥解密出正确的秘钥
TLS 握手的过程采用的是非对称加密
强缓存主要是由 Cache-control 和 Expires 两个 Header 决定的
Expires 的值和头里面的 Date 属性的值来判断是否缓存还有效。 Expires 是 Web 服务器响应消息头字段,在响应 http 请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求。 Expires 的一个缺点就是,返回的到期时间是服务器端的时间,这是一个绝对的时间,这样存在一个问题,如果客户端的时间与服务器的时间相差很大(比如时钟不同步,或者跨时区),那么误差就很大。
Cache-Control 指明当前资源的有效期,控制浏览器是否直接从浏览器缓存取数据还是重新发请求到服务器取数据。但是其设置的是一个相对时间。
指定过期时间: max-age 是距离请求发起的时间的秒数,比如下面指的是距离发起请求 31536000S 内都可以命中强缓存
Cache-Control: max-age=31536000
复制代码
表示没有缓存
Cache-Control: no-store
复制代码
有缓存但要重新验证
Cache-Control: no-cache
复制代码
私有和公共缓存
public 表示响应可以被任何中间人(比如中间代理、 CDN 等缓存) 而 private 则表示该响应是专用于某单个用户的,中间人不能缓存此响应,该响应只能应用于浏览器私有缓存中。
Cache-Control: private
Cache-Control: public
复制代码
验证方式:以下表示一旦资源过期(比如已经超过 max-age ),在成功向原始服务器验证之前,缓存不能用该资源响应后续请求
Cache-Control: must-revalidate
复制代码
Cache-control 优先级比 Expires 优先级高
以下是一个 Cache-Control 强缓存的过程:
Last-Modified 表示本地文件最后修改日期,浏览器会在 request header 加上 If-Modified-Since (上次返回的 Last-Modified 的值),询问服务器在该日期后资源是否有更新,有更新的话就会将新的资源发送回来
但是如果在本地打开缓存文件,就会造成 Last-Modified 被修改,所以在 HTTP / 1.1 出现了 ETag
Etag 就像一个指纹,资源变化都会导致 ETag 变化,跟最后修改时间没有关系, ETag 可以保证每一个资源是唯一的。 If-None-Match 的 header 会将上次返回的 Etag 发送给服务器,询问该资源的 Etag 是否有更新,有变动就会发送新的资源回来
If-none-match 、 ETags 优先级高于 If-Modified-Since、Last-Modified
第一次请求:
第二次请求相同网页:
协商缓存,假如没有改动的话,返回 304 ,改动了返回 200 资源
现在的200 (from cache) 已经变成了 disk cache (磁盘缓存)和 memory cache (内存缓存)两种
上面提到 HTTP 缓存相关,但是很多有时候,我们希望上线之后需要更新线上资源。
web 开发者发明了一种被 Steve Souders 称之为 revving 的技术。不频繁更新的文件会使用特定的命名方式:在 URL 后面(通常是文件名后面)会加上版本号。
弊端:更新了版本号,所有引用这些的资源的地方的版本号都要改变
web 开发者们通常会采用自动化构建工具在实际工作中完成这些琐碎的工作。当低频更新的资源( js/css )变动了,只用在高频变动的资源文件( html )里做入口的改动。
HTTP Cookie (也叫 Web Cookie 或浏览器 Cookie )是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。
Set-Cookie 响应头部和 Cookie 请求头部
Set-Cookie: <cookie名>=<cookie值>
复制代码
会话期Cookie是最简单的 Cookie :浏览器关闭之后它会被自动删除,也就是说它仅在会话期内有效。会话期 Cookie 不需要指定过期时间( Expires )或者有效期( Max-Age )。需要注意的是,有些浏览器提供了会话恢复功能,这种情况下即使关闭了浏览器,会话期 Cookie 也会被保留下来,就好像浏览器从来没有关闭一样
和关闭浏览器便失效的会话期 Cookie 不同,持久性 Cookie 可以指定一个特定的过期时间( Expires )或有效期( Max-Age )。
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT;
复制代码
标记为 Secure 的 Cookie 只应通过被 HTTPS 协议加密过的请求发送给服务端。
标记为 Secure 的 Cookie 只应通过被 HTTPS 协议加密过的请求发送给服务端。但即便设置了 Secure 标记,敏感信息也不应该通过 Cookie 传输,因为 Cookie 有其固有的不安全性, Secure 标记也无法提供确实的安全保障
通过 JavaScript 的 Document.cookie API 是无法访问带有 HttpOnly 标记的 cookie 。这么做是为了避免跨域脚本攻击( XSS )
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly
复制代码
Domain 和 Path 标识定义了 Cookie 的作用域:即 Cookie 应该发送给哪些 URL 。
Domain 标识指定了哪些主机可以接受 Cookie 。如果不指定,默认为当前的主机(不包含子域名)。如果指定了 Domain ,则一般包含子域名。
例如,如果设置 Domain=mozilla.org ,则 Cookie 也包含在子域名中(如 developer.mozilla.org )。
Path 标识指定了主机下的哪些路径可以接受 Cookie (该 URL 路径必须存在于请求 URL 中)。以字符 %x2F ("/") 作为路径分隔符,子路径也会被匹配。
例如,设置 Path=/docs ,则以下地址都会匹配:
/docs
/docs/Web/
/docs/Web/HTTP
复制代码
SameSite Cookie 允许服务器要求某个 cookie 在跨站请求时不会被发送,从而可以阻止跨站请求伪造攻击
Set-Cookie: key=value; SameSite=Strict
复制代码
None Strict Lax
在新版本的浏览器( Chrome 80 之后)中, SameSite 的默认属性是 SameSite=Lax 。换句话说,当 Cookie 没有设置 SameSite 属性时,将会视作 SameSite 属性被设置为 Lax —— 这意味着 Cookies 将不会在当前用户使用时被自动发送。如果想要指定 Cookies 在同站、跨站请求都被发送,那么需要明确指定 SameSite 为 None 。因为这一点,我们需要好好排查旧系统是否明确指定 SameSite ,以及推荐新系统明确指定 SameSite ,以兼容新旧版本 Chrome
更多 cookie 相关,可以查看我之前总结的一篇关于 cookie 的文章 前端须知的 Cookie 知识小结
跨域资源共享( CORS )是一种机制,它使用额外的 HTTP 头告诉浏览器,让运行在一个 origin ( domain ) 上的 web 应用被准许访问来自不同源服务器上的指定的资源
跨域资源共享标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站通过浏览器有权限访问哪些资源。
简单请求(不会触发 CORS 的预检请求)需要同时满足以下三点:
以下为一个简单请求的请求报文以及响应报文
简化以下:
请求首部字段 Origin 表明该请求来源于 http://foo.example
本例中,服务端返回的 Access-Control-Allow-Origin: * 表明,该资源可以被任意外域访问。如果服务端仅允许来自 http://foo.example 的访问,该首部字段的内容如下:
Access-Control-Allow-Origin: http://foo.example
复制代码
Access-Control-Allow-Origin 应当为 * 或者包含由 Origin 首部字段所指明的域名。
规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法。浏览器必须首先使用 OPTIONS 方法发起一个预检请求( preflight request ),从而获知服务端是否允许该跨域请求。
服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证相关数据)
预检请求中同时携带了下面两个首部字段:
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
复制代码
首部字段 Access-Control-Request-Method 告知服务器,实际请求将使用 POST 方法。首部字段 Access-Control-Request-Headers 告知服务器,实际请求将携带两个自定义请求首部字段: X-PINGOTHER 与 Content-Type 。服务器据此决定,该实际请求是否被允许。
预检请求的响应中,包括了以下几个字段
Access-Control-Allow-Origin: http://foo.example
// 表明服务器允许客户端使用 POST, GET 和 OPTIONS 方法发起请求
Access-Control-Allow-Methods: POST, GET, OPTIONS
// 表明服务器允许请求中携带字段 X-PINGOTHER 与 Content-Type
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
// 表明该响应的有效时间为 86400 秒,也就是 24 小时。在有效时间内,浏览器无须为同一请求再次发起预检请求。
Access-Control-Max-Age: 86400
复制代码
一般而言,对于跨域 XMLHttpRequest 或 Fetch 请求,浏览器不会发送身份凭证信息。如果要发送凭证信息,需要设置 XMLHttpRequest 的某个特殊标志位。比如说 XMLHttpRequest 的 withCredentials 标志设置为 true ,则可以发送 cookie 到服务端。
对于附带身份凭证的请求,服务器不得设置 Access-Control-Allow-Origin 的值为“*”。 这是因为请求的首部中携带了 Cookie 信息,如果 Access-Control-Allow-Origin 的值为“*”,请求将会失败。而将 Access-Control-Allow-Origin 的值设置为 http://foo.example ,则请求将成功执行。
CORS 涉及到的请求和响应头如下: HTTP 响应首部字段
HTTP 请求首部字段
Origin
Access-Control-Request-Method
Access-Control-Request-Headers
有感兴趣的朋友可以关注一下我的公众号:前端维他命,不定时更新优秀文章。
*请认真填写需求信息,我们会在24小时内与您取得联系。