下面的大部分问题来自Github的这个仓库,排名不分先后

46. html5中元素的data-*属性

用来存储用户自定义数据,可以通过Element.dataset.foo的形式读取。

47. CSS在前JS在后是一定的么

不。调试js,日志脚本,shim或polyfill以及动态修改整个页面style的JS代码需要放在<head>中。

48. progressive rendering

渐进式渲染技术是一种尽量快地将渲染结果展现给用户的技术。比如懒加载,优先渲染高优先级元素等手段。目标是将用户关心的内容优先展示出来,将页面信息一点点释放给用户。

49. normalize/reset CSS

知乎中一段说明写得太好,这里摘录下来。

CSS Reset 是革命党,CSS Reset 里最激进那一派提倡不管你小子有用没用,通通给我脱了那身衣服,凭什么你 body 出生就穿一圈 margin,凭什么你姓 h 的比别人吃得胖,凭什么你 ul 戴一胳膊珠子。于是 *{margin:0;} 等等运动,把人家全拍扁了。看似是众生平等了,实则是浪费了资源又占不到便宜,有求于人家的时候还得贱贱地给加回去,实在需要人家的默认样式了怎么办?人家锅都扔炉子里烧了,自己看着办吧。

Normalize.css 是改良派。他们提倡,各个元素都有其存在的道理,简单粗暴地一视同仁是不好的。body 那一圈确实挤压了页面的生存空间,那就改掉。士农工商,谁有谁的作用,给他们制定个规范,确保他们在任何浏览器里都干好自己的活儿。

Normalize.css是一种CSS reset的替代方案。Reset清除了所有浏览器默认样式,强调不同浏览器间的一致性。但是在我们需要浏览器默认样式时,就需要自己重头再来。Normalize是两位设计者研究不同浏览器默认样式差异后,设计的弥补浏览器bug保证一致性的CSS。现在大多数的网站已经抛弃了Reset.css而选择Normalize.css。

50. BFC是什么

BFC意为块格式化上下文(block formatting context),BFC表示一个区域,在区域内的所有子元素共同构成一个块级上下文。有4中手段可以触发BFC:

  • position: 不为static和relative
  • display: table-cell,table-caption,inline-block,flex,inline-flex
  • float: 不为none
  • overflow: 不为visible

相对来说通过overflow属性触发BFC对已有内容影响最小。利用BFC可以实现很多特性:

  • 消除box间margin的collapse现象
  • 包裹浮动元素,防止高度collapse
  • BFC不与其他元素相交叉,避免文字围绕图片,实现多栏布局

51. 清除浮动的一些手段

  • 在浮动元素后创建一个空的div标签,为之设置clear: both
  • 触发BFC,一般通过overflow属性触发
  • 使用伪类:after,设置clear: both

52. CSS sprites怎么实现和优缺点

将多张小图拼成大图后,通过background-imagebackground-position更改背景。优点是省流量,更换图标整体风格快;缺点是添加新图片麻烦。

53. 图片替代方案

图片替代指使用图片替代文字,同时保证图片可读,通常用于Logo。有下面几种方案:

  • display: none + background-image
  • text-ident: 9999px + background-image
  • margin-left: -9999px + background-image (盒模型很大,低效)
  • img alt属性
  • font-size或color: #fff

54. 解决browser-specific问题

写patches(独立的CSS文件)处理

55. 如何面对低等级浏览器

polyfill,graceful degration

56. 预处理语言的优缺点

优点:更加友善,更好的特性,语法糖
缺点:需要编译,应用场景局限

57. display有哪些可选值

  • none
  • inherit
  • initial
  • unset
  • block
  • inline
  • inline-block
  • flow
  • table
  • flex
  • grid
  • list-item
  • table-row-group
  • table-row
  • table-header-group
  • table-footer-group
  • table-cell
  • table-column-group
  • table-column
  • table-caption
  • inline-flex
  • inline-grid
  • inline-table
  • inline-list-item

58. translate和absolute的选择

两者并无优劣之分。只有应用场景的不同。translate可以实现复杂的位移和变形,absolute用在固定的定位时更方便

59. 视觉上隐藏一个元素

  1. position: fix + left: -9999px
  2. clip + width/height + overflow
  3. visibility: hidden

60. grid system

除开最新的Grid布局。可以通过inline-block + margin实现。grid布局目前支持还较差。这里是一篇很不错的介绍。

61. 高效的CSS写法

这里有一堆CSS编码风格。总体来说,要注意

  • 避免全局选择
  • 让选择器更加具体化
  • 减少不必要的选择器
  • 减少选择器的过深嵌套
  • 尽可能少使用表达式(即calc, rgba这些)
  • CSS放在头部

62. CSS匹配顺序

先构建DOM树,再从右至左地匹配CSS选择器

63. 盒模型

DOM元素以盒的形式呈现,包裹住真正的内容,有margin/border/padding/content四部分,width在默认情况下仅指content部分的宽度,height同理。若想改变盒模型,可以设置box-sizing属性

64. flex

flex是弹性布局。借助flex布局,可以很轻松地实现居中置右等使用默认方法难以实现的效果。flex布局有两个轴:主轴和交叉轴。元素在主轴方向上排布,在位置不够时,会沿交叉轴推挤到下一行或下一列。flex相关的CSS属性同时针对容器和项目。(下方加粗为默认值)

针对容器的属性有

  • flex-direction 主轴方向。可选row/column/reverse
  • flex-wrap 换行选项。可选nowrap/wrap/wrap-reverse
  • flex-flow 综合上面两个选项,如row wrap
  • justify-content 主轴上对齐方式。可选flex-start/flex-end/center/space-between/space-around
  • align-items交叉轴对齐方式。可选flex-start/flex-end/center/stretch/baseline
  • align-contents主轴间对齐方式。可选flex-start/end/center/space-between/space-around/stretch

针对项目的属性有

  • order 项目顺序,默认按照书写顺序排列
  • flex-grow 当容器主轴上长度足够时,该项目在主轴方向上的长度,默认为1,项目间按照该值比例分配
  • flex-shrink 当容器主轴上长度不够时,类似flex-grow处理
  • flex-basis 容器默认的主轴方向长度,也按比例分配
  • align-self 该项目的对齐方式

65. 适应式/响应式布局

responsive:响应式布局,使用同一种布局响应浏览器窗口的连续变化
adaptive:适应式布局,在视口特定大小时改变外观或样式,是离散的

66. DOM事件代理,冒泡和捕获两阶段

DOM事件代理是指,在DOM2级标准中,事件触发有捕获和冒泡两阶段,所以可以将事件监听器绑定在父节点上,减少EventListener的数目。细节可以参见之前做过的一则笔记

67. null, nudefined, undeclared三者的区别

null: 是Object类型,表示空对象,多用来表示还未赋值的对象
undefined: 是基础类型,表示没有定义的变量或属性
undecided: 只是一种称呼,表示没有用var, constlet声明的变量,默认为全局变量,应该避免出现这种情况

68. 匿名函数的应用场景

主要用作返回值或输入参数。

69. host object和native object的区别

前者是用户定义的对象类型,后者是环境自带的原生对象。尽量避免修改native object(包括增删改)。

70. Function.prototype.bind的使用场景

在setTimeout和listener handler中最常用到

71. feature detection/feature inference/UA

由于浏览器之间有各自的特性差异,这三种手段用于保证代码在浏览器间的兼容性。

  • feature detection 检测特性是否存在再使用,比较保险科学
  • feature inference 通过某特性是否存在推断另一特性是否存在,有风险,避免使用
  • UA 直接通过header中的User Agent来得到浏览器信息,建议迫不得已不使用

72. AJAX技术的优劣

优:用户体验好,局部刷新速度快,可以用于实现界面和数据分离
劣:相对来说较难调试,需要解决跨域问题,搜索引擎支持即SEO弱,会遇到移动端支持问题

73. JS templating

JS中的模板技术,如在backbone中使用的underscore的_.template方法。在xtemplate支持下,也可以在页面中指定<script type="x-template">的形式声明,

74. 如何理解不要改动built-in特性

浏览器和标准都是在不断变化的,此刻对built-in特性做的修改在之后浏览器或标注的呢更新后可能会埋下很深的坑。

75. 如何区分[]和{}

  • Object.Prototype.toString.call()
  • [].concat
  • instanceof
  • ES6中新增的方法Array.isArray

76. tenary operator

JS中唯一的三元操作符

77. DOM中attributes和properties的区别

节点的特性(attributes)用Attr类型表示。直观上讲,特性就是元素attributes属性中的节点,即在tag中声明的各特性名,以下面的标签为例:

1
<input type="text" value="John" />

该DOM节点有两个特性:typevalueAttr也是Node的一种,nodeType为2,Attr对象有三个属性

  • name 特性名称
  • value 特性的值
  • specified 特性是否指定在代码中,抑或是默认的

节点的属性(properties)则指对应的DOM对象的属性,不论是继承自Node或是Element类型的,还是自身类型自带的。比如上面同样的例子,该DOM节点具有children, childNodes, className等。

节点的属性和特性会有重合的部分,如id, type等,因DOM节点而异。上面的例子里,attribute中的value指声明在标签上的value默认值,而properties中的value则指该input标签当前的内容。

78. "use strict";

严格模式在ES5中引入,通过直接定义一个不赋给任何变量的上述字符串进入。可以选择在全局或是函数作用域内开启。

  • 严格模式下对默认模式下会静默出错的代码显式报错
  • 严格模式下禁止不合理的行为,如声明了两个一样的属性名
  • 严格模式还淘汰了一些属性,如arguments.calleearguments.caller; 同时,限制一些不安全的使用,如witheval
  • 严格模式抑制了this的值
  • 严格模式下,对未来版本可能用到的保留字禁止用户访问

不过,由于严格模式下代码的解析规则会不大一样,建议只在需要测试的特定函数内开启严格模式,

79. ready和load event的区别

  • ready在DOM元素加载完成后触发
  • load在页面所有资源请求完成后触发(包括图片、视频、音频等)

80. SPA的SEO优化

采用预渲染技术,或为爬虫专门准备静态页面

81. event loop,call stack和task queue

见这篇笔记

82. JavaScript中的对象和继承

见这篇笔记

不过,在ES6中引入了强类型OOP语言中传统的对象和继承语法。

  • 使用class关键字定义类,类中用constructor定义构造函数,使用publicprivate修饰成员级别
  • 可以在成员前指定getset为成员指定setter和getter函数
  • 通过extends实现继承

83. promise

见之前做过的一篇笔记

84. 提升有滚动条时的动画渲染性能

在CSS中为will-change属性指定动画要改变的CSS属性,参见MDN上的介绍

85. layout, painting, composition

浏览器解析,绘制,组合网页的过程。DOM操作可能会触发回流(reflow)或重绘(repainting),后者代价更小,建议减少频繁的DOM操作

86. 一些HTTP 1.1的header

  • Accept 接受文件的类型
  • Accept-Charset/Accept-Encoding 可以接受的文件字符集和编码
  • Age 从缓存实体产生到现在经历的时间
  • Allow 允许使用的HTTP方法
  • Cache-control 使用的缓存策略
  • Content-Encoding 响应体使用编码
  • Content-Lenght 响应体长度
  • Content-Range 响应体范围,用于部分下载(服务端的返回)
  • Content-Type 响应的媒体类型
  • Date 消息的发送时间
  • Etag html实体的唯一标识,用于缓存对比
  • Expires 缓存实体过期时间
  • Host 服务器的主机名,通常是请求资源的URL
  • Location 重定向的地址
  • Pragma 用于向后兼容还没有Cache-Control的HTTP1.0版本,通常只用作Pragma: no-cache
  • Range 请求资源的部分内容,一般用在多线程下载(客户端发起)
  • Referer 当前请求从哪个地址发起
  • Server 服务器端使用的软件信息
  • Transfer-Encoding 传输内容所用的协议类型
  • Upgrade 切换到额外的通信协议,服务端需要返回101状态码,并指定升级的协议名
  • User-Agent 请求发起自什么终端
  • Vary 列出一个相应字段列表,告知服务器当URL对应不同版本资源时,如何选择合适版本
  • Via 用在proxies中,表示使用的协议,版本以及起始端

更多header参考W3C文档

87. HTTP actions

  • OPTIONS 描述目标资源的通信选项
  • GET 获取数据
  • HEAD 类似GET,但是没有响应体
  • POST 将实体提交给服务器
  • PUT 用请求payload替换目标资源
  • PATCH 对资源部分修改
  • DELETE 删除指定资源
  • TRACE 沿着到目标资源的路径执行一个消息环回测试
  • CONNECT 建立一个到由目标资源标识的服务器的隧道

88. JS内存泄漏

  • 意外的全局变量
  • 被遗忘的setInterval
  • 脱离文档的DOM引用
  • 不合理的闭包

Chrome下可以通过Timeline/Profile选项卡查看内存使用情况,避免上述情况出现。

89. rem,em,px

Question 24

90. JS数据类型

基础类型:

  • Undefined
  • Null
  • Number(包括NaN Infinity)
  • Boolean
  • String

其余都是引用类型。更多参加这里

91. Object.assignObject.create

  • Object.assign 将传入变量的可枚举属性和已有属性合并
  • Object.create 创建一个以传入对象为__proto__的对象

92. 回流和重绘

还是参见how browsers work。为了减少回流,有下面一些推荐实践:

  • 一次性改变样式,如用class
  • requestAnimationFrame推迟回流
  • 虚拟DOM
  • 使用documentFragment

93. 排序算法

稳定:插冒归基;不稳定:快选堆希。实现略。

94. CSS/JS跨浏览器兼容问题

  • 明确产品的兼容版本方案,选择合适的技术栈
  • normalize.css polyfill/shim保证兼容
  • 在符合W3C标准浏览器下表现良好,旧浏览器下保证可用性,提示升级即可
  • CSS hack(特殊的选择器,条件样式表)

95. xss和csrf的防御

XSS(Cross Site Script,跨站脚本攻击),分为反射式,存储式,前者只对特定用户生效,如存储在个人资料中的脚本,后者对访问网站的所有用户生效,如攻击站点本身代码。防御转义时,不仅要监测<script>标签,对可以书写JavaScript的href属性,src属性,甚至imgonerror等事件也要做防御。

CSRF(Cross-site Request Forgery,跨站请求伪造),意为恶意网站通过用户存储的cookie,模拟向其他网站发起“合法”请求。需要注意下面两点

  • 不使用GET方法更新数据
  • 对于POST方法,通过后台生成随机的csrf_token注入到form<input type="hidden">标签中预防

总而言之,不信任用户的所有输入,对输入做处理,避免SQL注入和XSS攻击。


96. CSS属性继承

无继承性:

  • display
  • 文本属性:vertical-align/text-shadow/text-decoration
  • 盒模型属性
  • 背景相关属性
  • 定位属性:float clear top/left/right/bottom z-index
  • 轮廓内容:content outline

有继承性:

  • 字体属性
  • 文本属性:text-align/line-height/word-spacing/letter-spacing/color
  • 可见性:visibility
  • 表格列表:list-style
  • 光标:cursor

行内元素可继承:字体属性,除了text-indent/text-align
块元素可继承:text-align/text-ident

97. 移动端实现0.5px的border

  • 结合:before:after通过transform-originscale实现
  • 利用渐变background-image: linear-gradient
  • 直接使用backgroun-image

98. 随机打乱一个数组

Fisher-Yates shuffle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function shuffle(array) {
let counter = array.length;

// While there are elements in the array
while (counter > 0) {
// Pick a random index
let index = Math.floor(Math.random() * counter);

// Decrease counter by 1
counter--;

// And swap the last element with it
let temp = array[counter];
array[counter] = array[index];
array[index] = temp;
}

return array;
}

99. 移动端fixed定位bug

iOS中,在软键盘唤起后,fixed定位元素会失效,变为absolute定位。解决方案:主体部分设为height: 100%; overflow-y: scroll,通过absolute定位实现。

100. JS如何获知当前页面是否来自缓存

配合后台:后台传递时间戳到当前页面或cookie
无后台:通过xhr放送HEAD请求,得到返回的status code

101. 重复打印字符串

幂次叠加,substr切割字符串。

102. 正则匹配中?:?=的意思

  • ?: 非捕获匹配分组,匹配并出现在匹配结果中,但不作为子匹配返回
  • ?= 前瞻匹配,不出现在匹配结果中

更多参见问题16

下面的大部分问题来自Github的这个仓库,排名不分先后

18. Vue和React框架的对比

先说相同之处,Vue和React都是优秀的MVVM框架。具有响应式设计的种种特点,因此数据驱动是使用这两种框架不能忘的出发点。正如上一个问题提到的那样,开发者只需处理好数据,让框架去处理易错的UI。同时,组件化前端开发流程也是它们强调的一点,用组件化之名行代码重用之实,通过组件积木去构建整个页面。最后,它们不约而同地使用虚拟DOM树(vDOM)(Vue是在2.0引入的)抽象页面的节点,通过优化的diff算法减少频繁的DOM操作,减少交互的响应时间。另外,在最新的版本中,Vue和React的源码分别通过FlowTypeScript加入类型约束,增强可读性和健壮性。

两者同时也有MVVM框架共同的短板,把过多的渲染放在客户端,在页面元素复杂时,会加大交互的延迟。同时会影响首屏时间。

再说它们的不同之处,不同之处基本都在使用细节上。

  • Vue通常把网页模板写在template属性中,默认不支持JSX语法(可以通过插件支持)。React中则是写在render里
  • Vue糅合了Angular 1.x的特点,通过一些内置的自定义标签属性减少开发者的代码量。React则更加纯粹,少了这些预设的框架
  • Vue使用Object.defineProperty实现数据绑定,React通过setState显式更新依赖。前者更加酷和自然,但是在编程风格不好时,容易出现难以debug的错误。React推荐immutable Object,每次更新时使用新对象更新自身state,出错的概率更低。
  • Vue和React在组件生命周期上有些细微的差别。Vue的生命周期更加简明,在create,mount,update,destroy前后设置钩子函数,React在state改变前后也会有钩子函数
  • Vue的脚手架使用起来较React更加顺手(个人感觉),单文件.vue的组织方式,内部支持模板语言jade、ejs,预处理语言coffeescript,sass等。React的构建方式则更加自由。
  • React的社区较之Vue更为活跃些,流行的库要更多,脚手架中的库更多是爱好者自己开发的。Vue和React生态圈中都有路由和状态管理器的工具,其中Vue的都是官方开发的。

19. TCP三次握手,四次挥手过程

过程参考下图。

因为断开连接时数据可能并未传输完成,所以挥手时要多一步。

20. CSS预处理语言

Sass和Less。Sass支持变量的定义和使用,有语法控制结构,同时支持@mixin定义mixin和@function定义函数。更多介绍可以看我之前的一段笔记

less语法使用上和Sass相近,采用JavaScript实现。支持本地和在线解析。本人没有用过,更多特性参考官网

21. JS里的错误和异常处理

try catch语句块捕获错误,catch块接受一个参数作为错误对象。对象的message属性会给出错误的详细信息。catch后还可以接上finally关键字,finally语句块在错误处理后必定执行。throw语句可以抛出错误。

22. 闭包

闭包是JavaScript中比较有特色的概念。它和JS中的作用域链(见问题4)概念密切相关。闭包发生在函数中定义的函数,在外层函数退出后,其作用域环境通过作用域链仍然保存在存活的内部环境中。利用这种特点,可以实现诸如状态保存,封装等特殊性质。

值得注意的是,内层函数的不合理操作会导致内存泄漏。且大量使用闭包会导致性能问题。不要过度依赖闭包。

23. 列举一些RESTful操作名

GET/POST/DELETE/UPDATE/PUT等

24. 列举一些CSS中的长度单位

固定长度:

  • px 像素点
  • mm 毫米
  • cm 厘米
  • in 英寸
  • pt 磅(1/72英寸)
  • pc 活字(1/6英寸)

相对长度:

  • em 以字体大小(font-size)为单位
  • ex 以小写字母大小为单位
  • rem 以根元素(默认为<html>)的字体大小为单位,用于自适应布局
  • vh 视口高度的1/100
  • vw 视口宽度的1/100
  • vmin 视口宽高较小值的1/100
  • vmax 视口宽高较大值的1/100

25. 前端职责

狭义地来说是实现UI设计师的设计稿和UE、UX的交互细节。宏观来说,是借助浏览器在技术上处理和用户交互的所有环节。近些年来,借助native的帮助,前端还可以实现后台的业务部分。

26. 职业规划

27. 获取新鲜资讯的方式

28. 前端模块化

模块化规范包括CommonJS,CMD(SeaJS),AMD(RequireJS)等。我此前做过一个小型的笔记

29. position类型

static, relative, absolute, fix四种。

  • static 正常文档流
  • relative 正常文档流,指定top, left等CSS属性相对原位置移动
  • absolute 脱离文档流,相对上一个非static元素定位
  • fix 脱离文档流,相对html定位

通常建议使用正常文档流减少潜在bug。绘制复杂动画时,建议使用脱离文档流的布局。

30. 盒模型

盒模型意为所有渲染的元素都是一个个的矩形。矩形区域内包含margin, border, paddingcontent4层。CSS属性中只有两种盒模型:border-boxcontent-box。后者是默认值。可以通过box-sizing属性设置。

31. 使用原生JS发送AJAX

现代浏览器中都通过XMLHttpRquest对象实现Ajax请求。古老的IE浏览器会有不同的实现方法(目前已经很少见了),如:

1
2
new ActiveXObject("Microsoft.XMLHTTP");
new ActiveXObject("Msxml2.XMLHTTP.6.0");

通常使用XMLHttpRequest发送GET的方法如下:

1
2
3
var xhr = new XMLHttpRequest();
xhr.open("GET", "demo.php?id=1");
xhr.send()

发送POST和其他复杂请求时,需要设置request header,在需要接受AJAX返回时,还可以监听readyState的change事件
,大致像下面这样:

1
2
3
4
5
6
7
8
var xhr = new XMLHttpRequest();
xhr.open("POST", "ajax_post.php");
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
console.log("ok" + xhr.responseText);
}
}

XMLHttpRequest2中又引入了进度,跨域,中止等新特性。

另外,使用新的Fetch API也可以完成Ajax请求。Fetch提出的目标是提供访问和操纵HTTP的接口,异步的获取网络资源。它和XMLHttpRequst的区别是:

  • 即使响应是404或500,返回的Promise也会正常解决
  • 默认情况下,fetch在服务端不会发送或接受任何cookies

在Ajax需要跨域时,最常用的方法是使用jsonp的形式实现。不过目前通过XMLHttpRequest2或fetch也都能完成跨域请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function jsonp(url, success) {
var ud = '_' + +new Date,
script = document.createElement('script'),
head = document.getElementsByTagName('head')[0]
|| document.documentElement;
window[ud] = function(data) {
head.removeChild(script);
success && success(data);
};
script.src = url.replace('callback=?', 'callback=' + ud);
head.appendChild(script);
}
jsonp('http://soundcloud.com/oembed?url=http%3A//soundcloud.com/forss/flickermood&format=js&callback=?', function(data){
console.log(data);
})

31. 如何加快访问速度

这个问题实际上很大,可以从各个方面去优化

  • 压缩并打包资源文件
  • 使用CDN存储资源文件
  • 设置缓存
  • CSS sprites
  • 图片压缩
  • 图片懒加载
  • 合理的DOM层级设置
  • <script>标签设置deferasync或动态异步加载
  • 一般情况下,CSS在前,JS在后
  • 使用MVVM框架时,使用服务端渲染或预渲染

32. 前端的未来发展

随着大前端的风吹来,前端工作在横向和纵向都获得了更多的机会。横向上,由PC端到移动端甚至有界面展示的智能终端。不过后两者需要记住native库的帮助。纵向上,在NodeJS的帮助下,后台的部分业务功能抽离出来交由前端完成,前端对界面有完整的控制,数据通过接口的形式向后台索取。现在看来,身为一个前端工程师,不仅要对HTML,CSS,JavaScript老三样了如指掌,对Android或iOS也渐渐有了些要求。

33. CSS selector的优先级顺序

how browsers work中有介绍。根据CSS3 selectors specificity中的定义,一个选择器的优先级计算如下

  1. 如果声明来自于style属性,而不是带有选择器的规则,则记为1,否则记为0 (= a)
  2. 记为选择器中ID属性的个数 (= b)
    3.记为选择器中其他属性和伪类的个数 (= c)
  3. 记为选择器中元素名称和伪元素的个数 (= d)

将四个数字按a-b-c-d这样连接起来(位于大数进制的数字系统中),构成特异性。所使用的进制取决于上述类别中的最高计数。最终决定优先级顺序。简而言之就是,style > id > class > tag > pseudo class,统计情况下看个数,有!important时,以!important为准。

34. 连等的赋值顺序

问题是,有这样一段代码,问原理是什么:

1
2
3
4
var foo = { n: 1 };
var bar = foo;
foo.x = foo = { n: 2 };
console.log(foo.x); // undefined

我们来细化一下过程,首先我们要明确JavaScript中对象是引用类型:

  • 第一句里,首先创建了一个字面量对象,并将foo指向之
  • 第二句里,将foo赋值给bar,即bar也指向{ n: 1 }这个对象
  • 第三句里,又创建了一个{ n: 2 }的对象,首先寻找foo指向对象中是否有x属性,没有时则新建一个x属性指向这个字面量对象,接着改变foo存储的地址,指向这个新的对象。
  • 第四句里,由于新的对象没有x属性,foo.x将返回undefined

这时,如果我们console.log(bar)结果将是{x: {n: 2}, n: 1}`。

35. JavaScript的hoist行为

hoist特性又叫变量声明提升。是JavaScript中比较有特点的特性。意为将作用域中所有变量(包括函数)声明提升到语句的开头。如下面的语句

1
2
3
4
5
6
7
a = 1;
fun(a);

var a = 0;
function fun(num) {
return num + 1;
}

等同于

1
2
3
4
5
6
7
8
var a;
function fun(num) {
return num + 1;
}
a = 1;
fun(a);

a = 0;

需要注意的是当通过函数表达式声明函数时,会提示fun未定义。因为,此时的语句等同于

1
2
3
4
5
6
7
8
9
var a;
var fun;
a = 1;
fun(a);

a = 0;
fun = function(num) {
return num + 1;
}

最后,最佳的风格是所有的变量先声明再使用。这也是JSLint和JSHint等linter工具推荐的。

36. 优雅降级和渐进增强

又名graceful degration和progressive enhancement。是两种开发的思路。前者意为针对最高级的浏览器设计开发,再保证向下兼容;后者意为针对低版本浏览器设计,保证基础性能,再追对现代浏览器追加效果,提高用户体验。

37. 优化网页资源

  1. 使用CDN
  2. 分布存放
  3. CSS sprites
  4. disable etag

38. 浏览器一次可以从同一domain加载多少资源

2~8,因浏览器而异。这里有个非常全的表。

39. 轮播图设计思路

可以用display实现,配合渐变效果。需要看到幻灯片移动时,可以结合transform和scale实现,配合overflow: hidden

40. CSS3的部分新特性

  • border: border-radiusborder-image
  • background: background-sizebackground-origin
  • text: font-facetext-overflowtext-shadowword-breakword-wrap
  • transform: translaterotateskewscale
  • transition: transitionanimation
  • other: box-shadow

41. ARIA

ARIA全称Accessible Rich Internet Application。主旨是提升网页易用性,方便有阅读障碍的人使用。比较常用的属性有

  • role 当前元素的作用,
  • <label>, aria-label 元素名
  • aria-hidden 是否隐藏

42. CSS animation和JavaScript animation

前者方便简单,通过keyframe就可以画出动画,且浏览器会做一些优化,因此性能也比较好。后者更加灵活,可以暂停和反转,且支持交互性。更详细的分析参考这里

43. doctype是什么

doctype出现在HTML4.1。用于规范HTML和XML文档格式,在推出时,HTML有3种标准可以选择:strict, transitional, frameset。

1
2
3
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">

在HTML5中,只有一种doctype可以选择,那就是html:

1
<!DOCTYPE html>

44. standard mode和quirks mode

分别是标准模式和怪异模式。由于历史原因,为了兼容标准建立前就已存在的古老网站,浏览器存在着两种解析网页的模式。在怪异模式下,排版会模拟Navigator 4与Internet Explorer 5的非标准行为。为了支持在网络标准被广泛采用前,就已经建好的网站,这么做是必要的。在标准模式下,行为即(但愿如此)由HTML与CSS的规范描述的行为。在<!DOCTYPE>中指定html将自动启用标准模式。这里有更多介绍。

45. XHTML和HTML

XHTML伴随HTML4.01一同提出,使用更加严谨的语法。MIMEtype为application/xhtml+xml,比如:html元素需要有xml相关属性,元素名必须是小写字母,元素属性用"包围不能为空值,在内容里不能有&, 需要转义,包括其他特殊字符<>,空元素以/>结尾。由于语法过于严苛,使用的人不多。未推出的XHTML1.1便被html5取代。

下面的大部分问题来自Github的这个仓库,排名不分先后

1. JS中如何定义自定义事件,实现订阅/发布模式

明确需求:可以通过onemit绑定和触发事件。

方案:创建全局事件管理器events,构建事件名和回调函数数组的键值对。onemit分别写和读events。大概像下面这样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var EventUtil = {
// 全局事件管理
var events = {},
// 注册事件
on = function (type, handler) {
if (events[type]) {
events[type].push(handler);
} else {
events[type] = [handler];
}
},
// 触发事件
emit = function (type) {
if (!events[type]) return;
for (var i = 0, len = events[type].length; i < len; i++) {
events[type][i];
}
};
};

当使用Object.assign实现继承时,会出现events共享的问题。可以通过在第一次调用on时,通过Object.defineProperty的方式创建避免共享。

2. js中的this

首先,this永远是对象。

  • 全局上下文内,this全局对象
  • 函数上下文内,根据调用场景分情况讨论
    • 直接调用:全局对象
    • 通过对象的方法调用:调用方法的对象
    • 构造函数中:即将被创建的对象,有return语句时,以return的返回值为准
    • call和apply:传入的第一个值
    • bind方法:永久绑定到第一个参数

3. js跨域问题和解决方案

跨域(Cross-domain)是网景最初基于安全性考虑提出的策略。意为不同域名不同协议不同端口间的Ajax通信是被禁止的。根据使用需求,可以分为跨站请求资源和跨页面共享资源(我自己发明的说法)

跨站请求资源

  • jsonp(json with padding)跨域,利用了<script>标签的可跨域完成,自己写一遍就能搞懂
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function getJSONP(url, success) {

    var ud = '_' + +new Date,
    script = document.createElement('script'),
    head = document.getElementsByTagName('head')[0]
    || document.documentElement;

    window[ud] = function(data) {
    head.removeChild(script);
    success && success(data);
    };

    script.src = url.replace('callback=?', 'callback=' + ud);
    head.appendChild(script);
    }
  • CORS,使用CORS进行跨域请求时,在现代浏览器中已经可以像普通Ajax请求那样使用XMLHttpRequest即可,可以参考这个。需要后台服务器支持
  • 后台反向代理,需要一台中转的服务器
  • 建立websocket通信,需要后台服务器支持

跨页面共享资源,结合<iframe>有以下几种方案

  • 修改document.domain,使两个页面位于同一域名下,注意只能从精确->模糊修改域名
  • 通过window.name传递消息,利用了iframe location变化后,window.name不变的特点
  • location.hash
  • html5中的postMessage API在不同window间传递消息

这里附上一个链接.

4. js的作用域链

这是JavaScript最有特点同时也是最基础的内涵之一。红宝书和犀牛书都做了详尽和透彻的解释。这个问题理解了,什么是闭包就能很好地理解了。

执行环境是JavaScript中最为重要的一个概念。执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。虽然我们便习得代码无法访问这个对象,但解析器在处理数据时会在后台使用它。

全局执行环境是最外围的一个执行环境。…。某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁(全局环境知道应用程序退出时才会销毁)
每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行后,栈将其环境弹出,把控制权返回给之前的执行环境。ECMAScript程序中的执行流正是有这个方便的机制控制着。

当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,时钟都是当前执行的代码所在环境的变量对象。如果这个环境是函数,则将其活动对象做为变量对象。活动对象在最开始时只包含一个变量,即arguments对象(这个对象在全局环境中是不存在的)。作用域链中的下一个变量对象来自包含(外部)环境,而在下一个变量对象则来自下一个包含环境。这样,一直延续到全局执行环境;全局执行环境的变量对象始终都是作用域链中最后一个对象。

5. Function.callFunction.apply的区别

callapply同为改变this的方法,前者一个一个接收输入参数,后者以数组的形式接收。

6. 浏览器渲染页面的原理

可以参考经典的文章how browsers work,或者中译版.

7. 列举一些HTML5的改进

可以参考MDN给出的summary。比如:

  • 语义化,语义化标签<header>, <article>,语义化元素<figure>, <time>等和新的多媒体标签<audio>, <video>
  • 网络通信,Websocket,WebRTC
  • 图像,Canvas和WebGL
  • 离线存储,Storage接口和IndexDB
  • 性能,Web Worker,XMLHttpRequest2(支持进度等新特性),History API,Fullscreen,PointerLock,requestAnimationFrame等
  • CSS,CSS3的特性,有些甚至演进到了Level 4

8. HTML5中的定位API

Geolocation API,新的API,红宝书中有提到。通过navigator.geolocation对象实现,允许用户提供自己的所在地理位置。需要用户确认同意才可使用。最常用的方法是getCurrentPosition()。这个方法接受三个参数——成功回调、可选的失败回调、可选的选项。

类似的不常见的API还有Battery APIFile APIperformance等。

9. 一些前端框架的双向绑定原理

不是所有框架都提倡双向绑定。有的框架如Angular使用数据双向绑定,适合于表单很多的站点,React和Vue这样的使用的是单向绑定。在单向绑定背景下,可以通过addEventListener实现双向绑定。

实现原理上分为几种:

  • 发布-订阅模式,显式地绑定事件和监听函数。backbone就是这么做的,显式地通过Model.set修改数据触发change事件来更新视图。React也是通过setState显式地触发虚拟DOM树更新和重新渲染的。
  • 脏检查(digest cycle),通过特定事件触发脏检查。脏检查即一种不关心你如何以及何时改变的数据,只关心在特定的检查阶段数据是否改变的数据监听技术。过程大致是$update或其他手段触发digest阶段,遍历通过$watch绑定的watcher。对比值是否改变触发更新。优点是无需知道更改数据的方式,可以统一更新view,缺点是watcher较多时会有严重的性能问题。
  • 数据劫持Object.defineProperty,Vue使用这种方式实现隐式的绑定(当然在具体实现中复杂了许多)。这么做的问题是版本只支持到IE9+,且在数组更新时有所局限。

10. webpack的配置文件写法

除了常用的entry, output, module, plugins外,webpack的使用方法实在太多,建议去官网查看完整的配置信息。

11. node文件和网络API

文件操作上,常用的有fs.readFileSyncfs.writeFileSync,或通过流的方式使用fs.createReadStreamfs.createWriteStream。还有pipe将流连接在一起。除此之外,pathjoinnormalize常用在处理文件路径。

和网络操作相关的包包括http, https, url, querystring, zlib等。其中前两个包更为常用,尤其是http.createServer方法。

另外,在进程上有process, child_process等包。这里 有一篇文章做了比较详细的介绍。当然,有空最好还是去官方文档.

12. 和@import的区别

它们的最常见的使用方式都是引入CSS文件到html中。它们的区别在于

  • link是XHTML标签,除了加载CSS外,还可以引入RSS等其他资源;@import属于CSS范畴,只能加载CSS。
  • link引用CSS时,在页面载入时同时加载;@import需要页面网页完全载入以后加载。
  • link是XHTML标签,无兼容问题;@import是在CSS2.1提出的,低版本的浏览器不支持。
  • 由于link是标签,可以通过JavaScript控制来改变样式,后者不行。

13. cookie,localStorage和sessionStorage的区别和联系

cookie设计的初衷是用来为无状态的http访问添加用户状态消息的。大小4KB以下,会携带在请求头中。大多包含敏感信息,会和服务器端的session配合使用。

Storage API是HTML5的新API。又可以细分为localStorage和sessionStorage。它们一般只存储在客户端,用来缓存用户非敏感数据,大小因浏览器而异,容量约可达到5MB。sessionStorage在浏览器关闭后清除,localStorage则在超过时限或手动clear后清除。

cookie中的内容很少变化,且最好秘文储存,并通过HttpOnly添加限制(后台修改set-cookie头)。Storage则很可能会频繁读写。

14. HTTP状态码

根据状态码开头的数字确定状态码类型。下面列举一些常用的。

1xx 信息:这一类型的状态码,代表请求已被接受,需要继续处理。这类响应是临时响应。

  • 100 继续:客户端应当继续发送请求。
  • 101 切换协议:将通过Upgrade消息头通知客户端采用不同的协议来完成这个请求。

2xx 成功:这一类型的状态码,代表请求已成功被服务器接收、理解、并接受。

  • 200 OK:请求已成功,请求所希望的响应头或数据体将随此响应返回
  • 201 已创建:请求已经被实现,而且有一个新的资源已经依据请求的需要而创建,且其URI已经随Location头信息返回
  • 202 已接受:服务器已接受请求,但尚未处理
  • 204 No Content:服务器成功处理了请求,但不需要返回任何实体内容,用户浏览器应保留发送了该请求的页面
  • 205 Reset Content:和204的唯一不同是返回此状态码的响应要求请求者重置文档视图
  • 206 服务器已经成功处理了部分GET请求。该请求必须包含Range头信息来指示客户端希望得到的内容范围,多用于下载工具

3xx 重定向:这类状态码代表需要客户端采取进一步的操作才能完成请求。通常,这些状态码用来重定向,后续的请求地址(重定向目标)在本次响应的Location域中指明。

  • 300 多选择:被请求的资源有一系列可供选择的回馈信息,每个都有自己特定的地址和浏览器驱动的商议信息。用户或浏览器能够自行选择一个首选的地址进行重定向。
  • 301 永久移动:被请求的资源已永久移动到新位置
  • 302 临时移动:请求的资源现在临时从不同的URI响应请求
  • 303 重定向:对应当前请求的响应可以在另一个URI上被找到,而且客户端应当采用GET的方式访问那个资源
  • 304 如果客户端发送了一个带条件的GET请求且该请求已被允许,而文档的内容(自上次访问以来或者根据请求的条件)并没有改变
  • 305 使用中介:被请求的资源必须通过指定的代理才能被访问

4xx 客户端错误:代表了客户端看起来可能发生了错误,妨碍了服务器的处理

  • 400 无法理解的请求:由于包含语法错误,当前请求无法被服务器理解
  • 401 需要验证:当前请求需要用户验证。响应必须包含一个适用于被请求资源的WWW-Authenticate信息头用以询问用户信息。
  • 403 禁止访问:服务器已经理解请求,但是拒绝执行它
  • 404 未找到:请求所希望得到的资源未被在服务器上发现
  • 405 方法不允许:请求行中指定的请求方法不能被用于请求相应的资源,响应中必须返回一个Allow头信息用以表示出当前资源能够接受的请求方法的列表
  • 406 头部不对:请求的资源的内容特性无法满足请求头中的条件
  • 408 请求超时:客户端没有在服务器预备等待的时间内完成一个请求的发送
  • 411 需要指定长度:服务器拒绝在没有定义Content-Length头的情况下接受请求
  • 413 请求实体太长
  • 414 URI太长

5xx 服务器错误:代表了服务器在处理请求的过程中有错误或者异常状态发生

  • 500 内部错误:一般来说,这个问题会在服务器的代码出错时出现
  • 501 未实现:服务器不支持当前请求所需要的某个功能
  • 502 Bad GateWay:作为网关或者代理工作的服务器尝试执行请求时,从上游服务器接收到无效的响应
  • 503 服务不可达:由于临时的服务器维护或者过载,服务器当前无法处理请求。这个状况是临时的,并且将在一段时间以后恢复。
  • 504 网关超时:作为网关或者代理工作的服务器尝试执行请求时,未能及时从上游服务器或者辅助服务器收到响应
  • 505 HTTP协议版本不正确

15. URL去参数

location.search, location.href, location.origin分别代表url中的querystring,完整url和域名。再结合location.pathname, location.portlocation.protocol可以得到任意想要的URL参数。

另外,新的APIURLSearchParams中有些方法可以对querystring做方便的增删改查的操作。

  • append增加一个检索参数
  • delete删除一个检索参数
  • get获取检索参数的第一个值
  • getAll获取检索参数的所有值
  • has检查是否存在某检索参数
  • set设置一个检索参数的新值,会覆盖原值
  • keysvalues分别返回键和值组成的数组

16. js中的正则匹配

js中的正则匹配和Perl的正则匹配规则基本类似。在js中,使用一个正则表达式字面量,由包含在斜杠之间的模式组成。正则表达式同时也是RegExp对象。除了简单模式外,考察对正则表达式的熟悉在它的特殊字符使用上。

一些常见的特殊字符:

  • \ 用于转义

  • ^ 用于匹配开始或表示一个反向字符集(如[^xyz]

  • $ 用于匹配结尾

  • * 匹配前一个表达式0或多次 = {0,}

  • + 匹配前一个表达式1或多次 = {1,}

  • ? 匹配0或1次 = {0,1};紧跟量词后使匹配非贪婪

  • . 匹配除换行符外任何单字符

  • (x) 捕获匹配,会包括在最后结果中,也可以通过$1, $n来访问

  • (?:x) 非捕获分组,匹配但不捕获

  • x(?=y) 断言匹配,捕获后跟y的x

  • x|y 匹配x或y

  • {n} 量词,匹配n次,还有{n,m}和{n,}的用法

  • [xyz] 字符集,可以使用-连接,如[x-z]

  • \d 一个数字

  • \D 一个非数字

  • \s 一个空白字符,包含空格,制表符,分页符,换行符

  • \S 一个非空白字符

  • \w 一个单字字符,等价于[A-Za-z0-9_]

  • \W 一个非单字字符

另外,正则表达式还有几个可选参数辅助搜索类型

  • g 全局搜索
  • i 不区分大小写
  • m 多行搜索
  • y 粘性搜索

有一些方法用于和正则表达式相关

  • exec 在字符串中执行匹配,返回匹配结果
  • test 测试是否能匹配RegExp,返回true或false
  • match 对字符串执行查找匹配的String方法,返回匹配结果
  • search 在字符串中测试匹配,返回位置索引或-1
  • replace 在字符串中执行查找匹配,并使用替换字符串替换匹配的子字符串
  • split 使用一个正则表达式或字符串分割一个字符串,并储存在数组中

常见的考法有,书写一个邮箱或手机号的正则表达式:

  • 邮箱 /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
  • 手机号 /^0*(13|15|18|14|17)(\d{9}|\d-\d{3}-\d{5}|\d-\d{4}-\d{4}|\d{2}-\d{3}-\d{4}|\d{2}-\d{4}-\d{3})$/

17. MVC和MVVM框架

框架模式不是一门写代码的学问,而是一门管理与组织代码的学问。其本质是一种软件开发的模型。与设计模式不同,设计模式是在解决某类特定问题时总结抽象出的公共方法,是方法论的范畴,一种框架模式往往使用了多种设计模式,且和技术栈有耦合的关系。

视图(View)从本质上讲是数据在图像上的一种体现和映射。用户在操作图像时可以达到操作数据的目的,在数据更改后,需要重新将数据映射到视图上。这实际上就是MVC的出发点。

  • View: 放置视图相关的代码,原则上里面不应该有任何业务逻辑。
  • Controller: 放置视图与模型之间的映射,原则上这里应该很薄,他只放一些事件绑定相关的代码(router),但并不实现真正的功能,他只是一个桥梁。
  • Model: 这里的model不是说实体类,它是主要实现业务逻辑的地方。

开发流程是先创建视图组件,再将之关联到Model上,通过View修改Model中的值时,Model会触发绑定在之上的所有View的更新。Backbone是个典型的例子。这么做部分分离了视图和逻辑。但是,在情况复杂时,Model的代码量将会大大膨胀。

MVP因此而生,其中Presenter(分发器)代替了原来的Controller,分担了Model的部分功能。针对上面的问题,Presetner隔断了Model和View,当M改变时,会通知P去更新视图。业务逻辑和绑定逻辑从V和M中分离出来到P中。使得MVP三方分工更加鲜明。绝大多数的PHP框架都是MVP类型的。

MVVM是Model-View-ViewModel的缩写。在MVVM中,View和ViewModel是双向或单向数据绑定的关系。当ViewModel反应了Model中的数据模型,并绑定到视图属性上,反过来,视图属性变化后也会通过ViewModel影响Model。React,Vue这些流行的前端框架都是MVVM类型的。

不管是MVC还是MVP或MVVM,他们都是数据驱动的。核心上基于M推送消息,V或P来订阅这个模型。使用者需要维护的不再是UI树,而是抽象的数据。当UI的状态一旦多起来,这种框架模式的优势就很明显了。

本文基于Express 4.15.2

我之前的Express学习笔记还在这里

Express常被用来进行Node.js后台的快速搭建。它仅仅对Node.js进行了简单的封装,结合中间件等很自然和好用的概念,很容易上手和学习。Express的API主要包括Application,Request,Response,Router几个部分,这是Express代码主要实现的部分。在我看来,Express贵在它的中间件,它提供了足够自由的空间但也做出规范,提供req, res, next, err给中间件操作。它的生态系统也是围绕这个展开的。

创建服务器

让我们从头回忆,用Express创建一个简单的服务器。像下面这样。

1
2
3
4
5
6
7
8
9
10
var express = require('express')
var app = express()

app.get('/', function (req, res) {
res.send('Hello World!')
})

app.listen(3000, function () {
console.log('Hello world!')
})

先通过构造函数创建一个Express应用(Application)。接着为它指定路由规则。最后通过app.listen()的方式启动服务器。对比下Node.js原生的写法:

1
2
3
4
5
6
var http = require('http');

var server = http.createServer(function(request, response) {
res.write('Hello world');
res.end();
}).listen(3000);

那么Express的app究竟是什么,路由中间件又是如何绑定上去的。这些问题需要通过源码来解答。

代码结构

Express代码整体设计并不复杂(相对于Vue这样的复杂设计),比较容易看懂,一些常见的功能已经事先写成依赖包抽取出来,如debug(打印debug信息)和deprecate(显示API已废弃)等。Express的源码部分位于lib/路径下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
...
- lib/
- middleware/
- init.js
- query.js
- router/
- index.js
- layer.js
- route.js
- application.js
- express.js
- request.js
- response.js
- utils.js
- view.js
...
- index.js
...
1
2
...
module.exports = require('./lib/express');

根目录下的index.js是整个项目的入口,所做的工作只是引入lib/中的express.jslib/目录下middleware目录下放置了内置的中间件,router中放置中间件的功能实现。下面的几个文件中

  • application.js 应用的定义,app对象的API
  • express.js,对app,router等功能的封装
  • request.js和response.js是对http中res以及req的封装和增强
  • utils.js 常用工具函数的封装
  • view.js 建立拓展名和渲染引擎的联系

Application

express.jsApplication.js大致告诉我们了expressapp究竟为何物。

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
//express.js
exports = module.exports = createApplication;

function createApplication() {
// 创建一个app
var app = function(req, res, next) {
app.handle(req, res, next);
};

//继承EventEmitter和Application.js中定义的app对象
mixin(app, EventEmitter.prototype, false);
mixin(app, proto, false);

...
// 初始化app
app.init();
return app;
}

/*
* 一些暴露公用接口的操作
*/

/*
* 告知一些方法已废弃
*/

上面的mixin引入自merge-description,功能非常简单——通过描述符融合两个对象并返回,源码也很简单,主要由getOwnPropertyDescriptordefineProperty方法实现,感兴趣的可以一看。

app通过mixin继承了两个预定义对象,其中EventEmitter来自Node.js的API,继承后app将获得事件发布订阅功能。protoApplication.js导出定义。其中定义了app.listen()方法。

1
2
3
4
app.listen = function listen() {
var server = http.createServer(this);
return server.listen.apply(server, arguments);
};

综上就很明白了

  • 首先,express()返回的app其实是一个函数,可以接受req, res以及定义在Express中的next
  • 之后,app.listen()方法实际上执行了http.createServer(app).listen()

总结,**express()是个工厂函数,负责生产作为http.createServer(app)的参数**。之后,让我们继续深入看看Application.js中的代码。

app的工作主要是在Application.js中进行的。下面介绍一些它的属性和相关方法

  • cacheObject,缓存视图信息
  • engineObject,视图渲染引擎
    • engine(ext, fn),绑定拓展名和渲染方法,返回app自身用于链式调用
  • settings,app的设置信息,可以通过setget方法设置和获取。
    • get(setting)(或set(setting))获取配置值,这里是完整的配置表
    • set(setting, value)设置配置值
    • enabled(setting)disabled(setting)语法糖,获取Boolean类型配置值
    • enable(setting)disable(setting)语法糖,设置Boolean类型配置值
  • locals在app生命周期内存储用户信息
  • mountpath 顶级app挂载路径
    • path()返回app的挂载路径
    • use(fn)挂载中间件时可以指定第一个参数为挂载路径
  • router 该路由中间件属性在4.x中已废弃,由route(path)方法替代
  • _routerlazyrouter内部方法中加载,Router对象,中间件的容器。详细介绍在下文的Router一节中。

除了这些属性外,还有一些常见或内部使用的方法

  • defaultConfiguration初始化默认配置
  • lazyrouter自身路由的懒加载,原因写在了源码注释
  • handle(req, res, cb)最关键的app方法,用于分发请求完成路由功能。但它实际上只是对router.handle做了简单的封装。并在最后通过finalhandler来做兜底。finalhandler是个很轻量级的依赖包,用于生成404页面和记录错误。详细文档见github.
  • use(fn)最关键的app方法,用于添加中间件,主要逻辑是将arguments中的pathfn列表拆开来,再调用router的use方法注册
  • route(path)调用router的route方法注册一个路由中间件
  • param(name, fn)在自身的router对象中注册参数匹配回调
  • VERB(path, fn)为HTTP动词绑定路径和处理函数,主要功能也是由router代劳。有意思的是,HTTP动词是通过methods这个依赖包来返回的,这个包原理也非常简单——http.METHODS
  • all(path)用来匹配任意一个HTTP动词,其余功能和上面相同,实现上即将该路由中间件的所有动词都绑定上处理函数。
  • del 已废弃
  • render(name, options, cb)调用渲染引擎渲染模板
  • listen()即上文提到的方法,对http.listen的封装

中间件

中间件是Express设计的最精妙的地方,也是工作量最大的地方。之前Express利用了Connect来做这项工作。在当前版本中放在了router目录下去实现。整体来看,一个Express应用就是一系列的中间件首尾相接组成的。那么中间件是什么呢?用官网的话说就是“Middleware functions are functions that have access to the request object (req), the response object (res), and the next function in the application’s request-response cycle.”从code的角度来看,就是下面这样

1
2
3
4
var myMiddleware = function(req, res, next){
// do something
next();
};

如果是错误处理的中间件,需要将err传入为第一个参数

1
2
3
4
var myErrorMiddleware = function(req, res, next){
// do something
next();
};

所以,一个请求进入Express后,处理的流程大致是下面这样的:

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

---------------
| middleware1 |
---------------

---------------
| ... ... ... |
---------------

---------------
| middlewareN |
---------------

其中每个中间件能拿到reqres抑或err,在完成自身工作后调用next()执行下一个中间件。那么这些middleware是怎么放置在这条链上,请求又是如何通过next()一步步向下传递的呢?

中间件分两种:普通中间件和路由中间件。它们都通过handle方法处理;前者通过use方法注册,后者通过VERB(或all)方法注册。其中

  • 前者匹配所有以path开始的路径,而后者会精确匹配path路径;
  • 前者对于请求的方法不做要求,而后者只能处理指定的请求。
1
2
3
4
5
6
7
app.use('/user', function(req, res, next) {
// do something
});

app.get('/user', function(req, res, next) {
// do something
});

app层面

在app层面,即Application.js中,是由app.use(fn), app.VERB(或app.all)和app.handle(req, res, cb)完成的。而它们只是router的同名方法的简单封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
app.use = function use(fn) {
var offset = 0;
var path = '/';

// 由于这个API支持多参数,需要先判断第一个参数是否为路径
// 并通过offset存储结果
if (typeof fn !== 'function') {
var arg = fn;

while (Array.isArray(arg) && arg.length !== 0) {
arg = arg[0];
}

// first arg is the path
if (typeof arg !== 'function') {
offset = 1;
path = fn;
}
}

// 根据offset获取中间件函数
var fns = flatten(slice.call(arguments, offset));

...

// 调用lazyrouter
...

fns.forEach(function (fn) {
// 原生函数时,调用router.use注册即可
if (!fn || !fn.handle || !fn.set) {
return router.use(path, fn);
}

...

// 中间件为router或app时,handle方法才是我们需要的
// 储存req和res的app属性到处理它的app
router.use(path, function mounted_app(req, res, next) {
var orig = req.app;
fn.handle(req, res, function (err) {
setPrototypeOf(req, orig.request)
setPrototypeOf(res, orig.response)
next(err);
});
});

...
}, this);

return this;
};

其中flatten用于将多层嵌套数组扁平化为1层。可见到,app的use方法在做了预处理工作后,调用router的use完成注册工作。

需要路由中间件时,我们需要使用动词对应的方法(或all)去注册。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
methods.forEach(function(method){
// 根据形参个数避免app.get带来的歧义
app[method] = function(path){
if (method === 'get' && arguments.length === 1) {
// app.get(setting)
return this.set(path);
}

this.lazyrouter();

// 调用router的route方法构造一个路由中间件
var route = this._router.route(path);
// 为路由中间件特定方法指定处理函数
route[method].apply(route, slice.call(arguments, 1));
return this;
};
});

app的handle方法就比较简单了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
app.handle = function handle(req, res, callback) {
var router = this._router;

// 创建兜底的final handler
var done = callback || finalhandler(req, res, {
env: this.get('env'),
onerror: logerror.bind(this)
});

// 没有路由对象时,就可以结束了
if (!router) {
debug('no routes defined on app');
done();
return;
}

// 调用router的handle方法
router.handle(req, res, done);
};

Router层面


Express里中间件的具体实现在Router对象中。Router包含了Express中最为核心的概念。app中的许多API都是对Router API的简单封装。可以通过app._router来访问app的Router对象。Router的源码位于lib/router/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var proto = module.exports = function(options) {
var opts = options || {};

//Router本身也是一个函数
function router(req, res, next) {
router.handle(req, res, next);
}

// 将下文中所写的方法指定为router的prototype
setPrototypeOf(router, proto)

// Router的属性初始化
...
// 存储中间件的stack属性
router.stack = [];

return router;
};

Router对象有一个关键属性stack,为一个数组,存放着所有的中间件。每一个中间件都是一个Layer对象,如果该中间件是一个路由中间件,则相应的Layer对象的route属性会指向一个Route对象,表示一条路由。

注册

每次调用app.use()时,会执行router.use()stack属性添加一个新的中间件,这个中间件是由Layer对象包装的。

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
proto.use = function use(fn) {
// 处理输入参数,抽出path和fn
// 过程同app.use
...

if (callbacks.length === 0) {
throw new TypeError('Router.use() requires middleware functions');
}

// 循环fn数组,添加中间件
for (var i = 0; i < callbacks.length; i++) {
var fn = callbacks[i];

// 错误检测和打印信息
...

// 创建Layer对象
var layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: false,
end: false
}, fn);

// 指定route属性为undefined,表示是普通中间件
layer.route = undefined;

// 入栈
this.stack.push(layer);
}

return this;
};

对于路由中间件要复杂些,路由中间件是通过router.route()方法注册的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
proto.route = function route(path) {
// 创建新的Route对象
var route = new Route(path);

// 创建Layer对象
var layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: this.strict,
end: true
}, route.dispatch.bind(route)); // 绑定this为route对象

// 指定layer的route属性为当前的路由组件,表示是路由中间件
layer.route = route;

// 入栈
this.stack.push(layer);
return route;
};

看来,还需要去route.js中看看这个对象的定义。

1
2
3
4
5
6
7
8
9
10
function Route(path) {
this.path = path;
// 存放路由处理函数的stack
this.stack = [];

debug('new %o', path)

// 方法名和对应handler的键值对
this.methods = {};
}

可以看到,Route对象也有一个stack属性,为一个数组,其中的每一项也是一个Layer对象,是对路由处理函数的包装。我们可以把它理解成一个路由中间件对象。每次调用router.route()的时候,实际上是新建了一个layer放在router.stack中;并设置layer.route为新建的Route对象。

之后,通过route[method].apply(route, slice.call(arguments, 1))为特定方法绑定handler,route[method]定义在route.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
methods.forEach(function(method){
Route.prototype[method] = function(){
var handles = flatten(slice.call(arguments));

for (var i = 0; i < handles.length; i++) {
// handle即用户指定的处理函数数组
var handle = handles[i];

// 检测handle合法性
...
// 新建layer对象
var layer = Layer('/', {}, handle);
layer.method = method;

// 更新this.methods数组,并将layer入栈
this.methods[method] = true;
this.stack.push(layer);
}

return this;
};
});

即,当调用route.VERB()的时候,新建一个layer放在route.stack中。

通过上面的分析可以发现,Router其实是一个二维的结构。一个可能的router.stack结构如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
----------------
| layer1 |
----------------

---------------- layer2.route.stack ------------ ------------ ------------
| layer2 | ------------------> | layer2-1 |-->| layer2-2 |-->| layer2-3 |
---------------- ------------ ------------ ------------

---------------- layer3.route.stack ------------ ------------
| layer3 | ------------------> | layer3-1 |-->| layer3-2 |
---------------- ------------ ------------

----------------
| ...... |
----------------

----------------
| layerN |
----------------

Layer

Layer对象的构造函数如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Layer(path, options, fn) {
// 实现函数名前有无new返回相同的效果
if (!(this instanceof Layer)) {
return new Layer(path, options, fn);
}

debug('new %o', path)
var opts = options || {};

this.handle = fn;
this.name = fn.name || '<anonymous>';
this.params = undefined;
this.path = undefined;
this.regexp = pathRegexp(path, this.keys = [], opts);

// 设置特殊情况的快速匹配
this.regexp.fast_star = path === '*'
this.regexp.fast_slash = path === '/' && opts.end === false
}

Layer的属性和一些方法介绍如下

  • handle 用户指定的中间件函数
  • name 函数名
  • params 参数名,在执行match时赋值
  • path 路径名,在执行match时赋值
  • regexp 路径的正则表达形式,由pathRegexp转换完成
  • keys 路径匹配结果信息
  • route 路由中间件的Route对象,或undefined

上面看到,普通中间件和路由中间件都通过Layer的形式插入在stack中。尽管它们都有构造函数中声明的哪些属性,这两种Layer还是有所区别:

  • Router中的Layer对象具有route属性,如果该属性不为undefined,则表明为一个路由中间件;而Route中的Layer对象没有route属性
  • Route中的Layer对象具有method属性,表明该路由函数的HTTP方法;而Router中的Layer对象没有method属性
  • Route中的Layer对象的keys属性值均为[]regexp属性值均为/^\/?$/i,因为在Route模块中创建Layer对象时使用的是Layer('/', {}, fn)

请求处理

在中间件注册完成后,剩下的工作都是由app.handle()或者说router.handle()完成的。这部分代码比较复杂。大致结构如下

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
proto.handle = function handle(req, res, out) {

...

// middleware and routes
var stack = self.stack;

...

next();

function next(err) {
...

while (match !== true && idx < stack.length) {
...
}

// no match
if (match !== true) {
return done(layerError);
}

...
}

function trim_prefix(layer, layerError, layerPath, path) {
...
}
};

在初始化和预处理后,调用了next()函数。在next()函数中,主要的部分是while语句判断是否遍历完成整个stack,完成后执行done()。这部分代码如下

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
while (match !== true && idx < stack.length) {
layer = stack[idx++];
match = matchLayer(layer, path);
route = layer.route;

if (typeof match !== 'boolean') {
// hold on to layerError
layerError = layerError || match;
}

if (match !== true) {
continue;
}

if (!route) {
// process non-route handlers normally
continue;
}

if (layerError) {
// routes do not match with a pending error
match = false;
continue;
}

var method = req.method;
var has_method = route._handles_method(method);

// build up automatic options response
if (!has_method && method === 'OPTIONS') {
appendMethods(options, route._options());
}

// don't even bother matching route
if (!has_method && method !== 'HEAD') {
match = false;
continue;
}
}

其中layer表示当前中间件,调用matchLayer方法即layer.match(path)判断是否和当前路径匹配(这个过程会更新layer中的pathparams)。之后的逻辑如下:

  1. 如果match不为true,即中间件和路径不匹配,则处理下一个中间件
  2. 如果matchtrueroute不存在,表示不是一个路由中间件,执行continue,之后跳出循环
  3. 如果matchtrue,且route存在。说明是需要的中间件。
    3.1 如果has_methodfalse且HTTP方法为OPTIONS,则执行appendMethods(options, route._options())添加支持方法
    3.2 如果has_methodfalse且HTTP方法不为HEAD,则设置match为false,即该路由无法处理该请求,此时由于match依然满足循环条件,因此会对下一个中间件进行判断
    3.3 如果has_methodtrue,则由于match不再满足循环条件,因此会跳出循环

整体来看,循环的主要作用就是从当前下标开始找出第一个能够处理该HTTP请求的中间件。如果是非路由中间件,则只要匹配路径即可;如果是路由中间件,则需要同时匹配路径和HTTP请求方法。

while语句后,如果matchtrue,说明遍历完成,直接执行done()。否则将匹配中得到的pathparams交给process_params方法作参数预处理。

1
2
3
4
5
6
7
8
9
10
11
self.process_params(layer, paramcalled, req, res, function (err) {
if (err) {
return next(layerError || err);
}

if (route) {
return layer.handle_request(req, res, next);
}

trim_prefix(layer, layerError, layerPath, path);
});

从回调函数中可以看到,如果是路由中间件可以直接调用layer.handle_request(req, res, next)执行真正的中间件函数。如果是普通中间件,还需要在trim_prefix中对路径处理后才会调用layer.handle_request(req, res, next)

1
2
3
4
5
6
7
8
9
function trim_prefix(layer, layerError, layerPath, path) {
...

if (layerError) {
layer.handle_error(layerError, req, res, next);
} else {
layer.handle_request(req, res, next);
}
}

在路由中间件中,layer.handle_request(req, res, next)调用的中间函数实际上是route.dispatch方法,在Route对象内容分发请求,它的逻辑相对router.handle要简单

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
Route.prototype.dispatch = function dispatch(req, res, done) {
var idx = 0;
var stack = this.stack;
if (stack.length === 0) {
return done();
}

var method = req.method.toLowerCase();
if (method === 'head' && !this.methods['head']) {
method = 'get';
}

req.route = this;

next();

function next(err) {
// signal to exit route
if (err && err === 'route') {
return done();
}

// signal to exit router
if (err && err === 'router') {
return done(err)
}

var layer = stack[idx++];
if (!layer) {
return done(err);
}

if (layer.method && layer.method !== method) {
return next(err);
}

if (err) {
layer.handle_error(err, req, res, next);
} else {
layer.handle_request(req, res, next);
}
}
};

可以看到,next函数逻辑像下面这样

  1. 如果有错,直接调用done()传递错误
  2. 获取layer,如果不存在,调用done()
  3. layer和当前方法不匹配时,跳过当前layer,执行next(),继续下一个route函数
  4. layer和当前方法匹配时,根据有无错误执行layer.handle_error(err, req, res, next)或是layer.handle_request(req, res, next)

综上,一个请求到达时,流程顺序像下面这样

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

----------------
| layer1 |
----------------

----------------
| layer2 |
----------------

---------------- layer3.route.stack ------------ ------------ ------------
| layer3 | ------------------> | layer3-1 |-->| layer3-2 |-->| layer3-3 | ---
---------------- ------------ ------------ ------------ |
|
---------------------------------------------------------------------------

---------------- layer4.route.stack ------------ ------------
| layer4 | ------------------> | layer4-1 |-->| layer4-2 | ---
---------------- ------------ ------------ |
|
------------------------------------------------------------

----------------
| ...... |
----------------

----------------
| layerN |
----------------

每个中间件的处理过程则像下面这样

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

No --------------------
------| path match |
| --------------------
| ↓ Yes
| -------------------- Yes --------------------- No
| | has route |-------| http method match |------
| -------------------- --------------------- |
| ↓ No | Yes |
| -------------------- | |
| | process params |<----------------- |
| -------------------- |
| ↓ |
| -------------------- |
| | execute function | |
| -------------------- |
| ↓ |
| -------------------- |
----->| next layer |<---------------------------------
--------------------

在参数处理的过程中,每个参数的处理函数只会执行一次,并将结果保存在缓存中。在处理同一个请求的过程中,如果需要处理某个参数,会首先检查缓存,如果缓存中不存在,才会执行其处理函数。

内置中间件

middleware目录下,放置了两个Express内置中间件,在app.lazyRouter中自动加载。

1
2
3
4
5
6
7
8
9
10
11
app.lazyrouter = function lazyrouter() {
if (!this._router) {
this._router = new Router({
caseSensitive: this.enabled('case sensitive routing'),
strict: this.enabled('strict routing')
});

this._router.use(query(this.get('query parser fn')));
this._router.use(middleware.init(this));
}
};

其中第一个的作用是解析URL query,query(this.get('query parser fn'))用于设置URL query解析器。第二个的作用是将req和res分别暴露给对方,并让它们分别继承自express定义的app.requestapp.response

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
exports.init = function(app){
return function expressInit(req, res, next){
if (app.enabled('x-powered-by')) res.setHeader('X-Powered-By', 'Express');
req.res = res;
res.req = req;
req.next = next;

setPrototypeOf(req, app.request)
setPrototypeOf(res, app.response)

res.locals = res.locals || Object.create(null);

next();
};
};

express.js中,reqres分别继承自了request.jsresponse.js的导出对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function createApplication() {
...

// expose the prototype that will get set on requests
app.request = Object.create(req, {
app: { configurable: true, enumerable: true, writable: true, value: app }
})

// expose the prototype that will get set on responses
app.response = Object.create(res, {
app: { configurable: true, enumerable: true, writable: true, value: app }
})

...
}

其他

关于参数处理和视图渲染,我看得不是很仔细,就不再赘述了。有兴趣的可以自行去参考中的链接学习。

参考

说到CORS,要从下面是一段很常见的前端面试对话

问: 之前实践中有遇到过跨域的需求吗?

答: 遇到过。

问: 那你是怎么解决的呢?

答: 跨域吧,大概有8种方法,很平衡,jsonp,CORS,反向代理,Websocket。结合iframe,还有使用document.domain, window.name, location.hash, window.postMessage等方法。

jsonp

jsonp是此前最为常用的一种跨域方法,利用了<script>标签的非跨域性,实现起来大概是下面这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function jsonp(url, success) {

var ud = '_' + +new Date,
script = document.createElement('script'),
head = document.getElementsByTagName('head')[0]
|| document.documentElement;

window[ud] = function(data) {
head.removeChild(script);
success && success(data);
};

script.src = url.replace('callback=?', 'callback=' + ud);
head.appendChild(script);
}

jsonp('http://soundcloud.com/oembed?url=http%3A//soundcloud.com/forss/flickermood&format=js&callback=?', function(data){
console.log(data);
});

CORS

CORS(Cross-Origin Resource Sharing)是W3C规定的在客户端用来进行跨站通信的标准。随着XMLHttpRequest2的出现,大部分浏览器下,可以像普通同域那样使用xhr对象来发起跨域请求。

构造一个CORS请求

CORS被下列浏览器支持

  • Chrome 3+
  • Firefox 3.5+
  • Opera 12+
  • Safari 4+
  • Internet Explorer 8+

其中Chrome,FF,Opera,Safari都通过XMLHttpRequest2对象来实现CORS。不一般的IE通过XDomainRequest对象实现,不过工作方式和xhr对象大同小异。Nicolas Zakas(《JavaScript高级程序设计》的作者)写了一个helper函数,保证了浏览器兼容性:

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
function createCORSRequest(method, url) {
var xhr = new XMLHttpRequest();
if ("withCredentials" in xhr) {

// Check if the XMLHttpRequest object has a "withCredentials" property.
// "withCredentials" only exists on XMLHTTPRequest2 objects.
xhr.open(method, url, true);

} else if (typeof XDomainRequest != "undefined") {

// Otherwise, check if XDomainRequest.
// XDomainRequest only exists in IE, and is IE's way of making CORS requests.
xhr = new XDomainRequest();
xhr.open(method, url);

} else {

// Otherwise, CORS is not supported by the browser.
xhr = null;

}
return xhr;
}

var xhr = createCORSRequest('GET', url);
if (!xhr) {
throw new Error('CORS not supported');
}

withCredentials

标准的CORS请求默认不携带cookie,如果需要在请求中携带cookie信息,需要在为xhr对象指定withCredentials属性。

1
xhr.withCredentials = true

同时,服务器端也要在响应头上设置Access-Control-Allow-Credentials字段为true。像下面这样:

1
Access-Control-Allow-Credentials: true

值得注意的是,cookie同样遵守同源法则,即你的JS代码无法获取和设置远端的cookie。

发起CORS请求

CORS请求发起方式和xhr没有什么区别,调用xhr.send()即可。综合来看,一个End to End的例子像下面这样

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
// Create the XHR object.
function createCORSRequest(method, url) {
var xhr = new XMLHttpRequest();
if ("withCredentials" in xhr) {
// XHR for Chrome/Firefox/Opera/Safari.
xhr.open(method, url, true);
} else if (typeof XDomainRequest != "undefined") {
// XDomainRequest for IE.
xhr = new XDomainRequest();
xhr.open(method, url);
} else {
// CORS not supported.
xhr = null;
}
return xhr;
}

// Helper method to parse the title tag from the response.
function getTitle(text) {
return text.match('<title>(.*)?</title>')[1];
}

// Make the actual CORS request.
function makeCorsRequest() {
// This is a sample server that supports CORS.
var url = 'http://html5rocks-cors.s3-website-us-east-1.amazonaws.com/index.html';

var xhr = createCORSRequest('GET', url);
if (!xhr) {
alert('CORS not supported');
return;
}

// Response handlers.
xhr.onload = function() {
var text = xhr.responseText;
var title = getTitle(text);
alert('Response from CORS request to ' + url + ': ' + title);
};

xhr.onerror = function() {
alert('Woops, there was an error making the request.');
};

xhr.send();
}

背后

CORS背后的脏活累活包括额外的包头以及额外的报文,这些由浏览器帮我们代劳了。CORS请求分为“简单的请求”和“没那么简单的请求”。简单的请求包含下列特征:

  • HTTP方法名是GET, POST, HEAD之一
  • HTTP包头包括
    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • Content-Type为application/x-www-form-urlencodedmultipart/form-datatext/plain

这种普通的CORS请求可以不必选择CORS,而可以通过jsonp或表单提交的方式解决。剩下的不那么简单的请求则需要浏览器和服务器进行额外的报文交换(prelight request)。

我们先从一个简单的请求开始,利用上面封装好的函数,写出下面的一段代码

1
2
3
var url = "http://api.foo.com/cors",
xhr = createCORSRequest('GET', url);
xhr.send();

在它的背后的HTTP包头像下面这样,请注意其中的Origin字段。

1
2
3
4
5
6
GET /cors HTTP/1.1
Origin: http://api.bar.com
Host: api.foo.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0 ...

一个合理的CORS请求必须包含一个Origin包头。这个包头由浏览器添加,用户不可自行修改。这部分由协议,域名和端口三部分组成。三者任意一个与Host不一致就算做跨域。非跨域时,不同浏览器对这个字段的处理方式不同,如FF会省去Origin字段,Chrome和Safari会在POST/DELETE/PUT时包括这个字段。

幸运的是,在跨域时一定会带上这个字段。支持CORS会根据客户端的Origin是否在allow list中做出回应。下面是一个样例

1
2
3
4
Access-Control-Allow-Origin: http://appi.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8

其中前三个以Access-Control-开头的字段和CORS相关。

  • Access-Control-Allow-Origin(必须),这个字段必须附加在合法的CORS响应中,像上面例子所写的那样,或是设置为*表示允许任意源的请求(不过一般不会这样设置)
  • Access-Control-Allow-Credentials(可选),默认情况下,cookie不被包含在CORS请求中,设置此字段为true表示包含cookie在请求中。这个字段需要和XMLHttpRequest2中的withCredentials属性配合保证成功。
  • Access-Control-Expose-Headers(可选),XMLHttpRequest2的getResponseHeader()方法可以获取下面这些属性
    • Cache-Control
    • Content-Language
    • Content-Type
    • Expires
    • Last-Modified
    • Pragma
      如果想访问其他属性时,需要设置这个字段,属性间用逗号隔开。

当请求“没那么简单”时,比如徐需要使用PUT或DELETE,或是需要支持JSON的返回资源类型时,浏览器需要先发起一次prelight request。在收到服务器允许的回复后,真实的请求再发出。不过这个过程对于用户是透明的。下面是一个例子

1
2
3
4
var url = "http://api.foo.com",
xhr = createCORSRequest('PUT', url);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();

Prelight request如下:

1
2
3
4
5
6
7
8
OPTIONS /cors HTTP/1.1
Origin: http://api.bar.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.foo.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0 ...

除了我们上面已经提过的Origin字段外,又新增了两个字段

  • Access-Control-Request-Method 真实的HTTP请求方法。是始终包含在包头的
  • Access-Control-Request-Headers 用逗号分隔的真实HTTP请求的包头。

服务器接收到这些后,会根据方法和包头,结合Origin检验合法性。在验证合法后,返回Prelight Response

1
2
3
4
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8

它们的含义从名字中就可以推出一二

  • Access-Control-Allow-Origin(必须) 略
  • Access-Control-Allow-Methods(必须),用逗号分隔的支持的HTTP方法列表。尽管可能请求中只写了一种方法。这么做避免了多次请求
  • Access-Control-Allow-Headers (若客户端填写对应的头部则为必须),逗号分割的支持的所有头部类型
  • Access-Control-Allow-Credentials(可选) 略
  • Access-Control-Max-Age(可选),prelight response的缓存时间,以秒为单位。

Prelight requestPrelight response交换完成后,就是真正的请求和响应。此时的请求头部可以带上之前商议中所允许的字段,大致像下面这样:

1
2
3
4
5
6
7
PUT /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
X-Custom-Header: value
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

当请求成功发出时,可以在响应头部看到CORS相关的字段,如Access-Control-Allow-Origin。请求失败时,会在console上报错,但不会给出具体信息。

CORS from jQuery

关于使用jQuery发起CORS请求,可以参加后文参考的第一条或jQuery相关文档。

参考

0%