0%

前端知识体系梳理(二-3)—— JS-Web-API

第二部分的最后一篇,加油!

  • BOM操作
  • DOM操作
  • 事件
    • 事件绑定
    • 事件冒泡
    • 事件代理
  • Ajax
    • XMLHttpRequest
    • Fetch API
    • 跨域
  • 存储
    • cookie、localStorage、sessionStorage

BOM操作

BOM(浏览器对象模型)是浏览器本身的一些信息的设置和获取。
其在window下的属性有:

  • navigator
  • screen
  • location
  • history

获取浏览器特性(即俗称的UA)然后识别客户端,例如判断是不是 Chrome 浏览器

1
2
3
var ua = navigator.userAgent
var isChrome = ua.indexOf('Chrome')
console.log(isChrome)

获取屏幕的宽度和高度

1
2
console.log(screen.width)
console.log(screen.height)

获取网址、协议、path、参数、hash 等

1
2
3
4
5
6
// 例如当前网址是 https://juejin.im/timeline/frontend?a=10&b=10#some
console.log(location.href) // https://juejin.im/timeline/frontend?a=10&b=10#some
console.log(location.protocol) // https:
console.log(location.pathname) // /timeline/frontend
console.log(location.search) // ?a=10&b=10
console.log(location.hash) // #some

调用浏览器的前进、后退功能

1
2
history.back()
history.forward()

DOM操作

什么是DOM
浏览器就需要把 HTML 转变成 DOM,HTML 是一棵树,DOM 也是一棵树。对 DOM 的理解,可以暂时先抛开浏览器的内部因素,先从 JS 着手,即可以认为 DOM 就是 JS 能识别的 HTML 结构,一个普通的 JS 对象或者数组。

获取DOM节点
最常用的 DOM API 就是获取节点,其中常用的获取方法有:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 通过 id 获取
var div1 = document.getElementById('div1') // 元素

// 通过 tagname 获取
var divList = document.getElementsByTagName('div') // 集合
console.log(divList.length)
console.log(divList[0])

// 通过 class 获取
var containerList = document.getElementsByClassName('container') // 集合

// 通过 CSS 选择器获取
var pList = document.querySelectorAll('p') // 集合

property 和 attribute 的区别

property
DOM 节点本质上也是一个 JS 对象。因此,如下代码所示,p可以有style属性,有className nodeName nodeType属性。注意,这些都是 JS 范畴的属性,符合 JS 语法标准的。

1
2
3
4
5
6
7
8
9
10
var pList = document.querySelectorAll('p')
var p = pList[0]
console.log(p.style.width) // 获取样式
p.style.width = '100px' // 修改样式
console.log(p.className) // 获取 class
p.className = 'p1' // 修改 class

// 获取 nodeName 和 nodeType
console.log(p.nodeName)
console.log(p.nodeType)

attribute
property 的获取和修改,是直接改变 JS 对象,而 attribute 是直接改变 HTML 的属性,两种有很大的区别。attribute 就是对 HTML 属性的 get 和 set,和 DOM 节点的 JS 范畴的 property 没有关系。

1
2
3
4
5
6
var pList = document.querySelectorAll('p')
var p = pList[0]
p.getAttribute('data-name')
p.setAttribute('data-name', 'juejin')
p.getAttribute('style')
p.setAttribute('style', 'font-size:30px;')

而且,get 和 set attribute 时,还会触发 DOM 的查询或者重绘、重排,频繁操作会影响页面性能。

DOM 操作的基本API:
新增节点

1
2
3
4
5
6
7
8
9
10
var div1 = document.getElementById('div1')

// 添加新节点
var p1 = document.createElement('p')
p1.innerHTML = 'this is p1'
div1.appendChild(p1) // 添加新创建的元素(append 和appendTo是jquery的用法)

// 移动已有节点。注意,这里是“移动”,并不是拷贝
var p2 = document.getElementById('p2')
div1.appendChild(p2)

获取父元素

1
2
var div1 = document.getElementById('div1')
var parent = div1.parentElement

获取子元素

1
2
var div1 = document.getElementById('div1')
var child = div1.childNodes

删除节点

1
2
3
var div1 = document.getElementById('div1')
var child = div1.childNodes
div1.removeChild(child[0])

事件

事件绑定

1
2
3
4
5
6
var btn = document.getElementById('btn1')
btn.addEventListener('click', function (event) {
// event.preventDefault() // 阻止默认行为
// event.stopPropagation() // 阻止冒泡
console.log('clicked')
})
1
2
3
4
5
6
7
8
9
10
// 通用的事件绑定函数
function bindEvent(elem, type, fn) {
elem.addEventListener(type, fn)
}
var a = document.getElementById('link1')
// 写起来更加简单了
bindEvent(a, 'click', function(e) {
e.preventDefault() // 阻止默认行为
alert('clicked')
})

事件冒泡

1
2
3
4
5
6
7
8
9
10
11
12
<body>
<div id="div1">
<p id="p1">激活</p>
<p id="p2">取消</p>
<p id="p3">取消</p>
<p id="p4">取消</p>
</div>
<div id="div2">
<p id="p5">取消</p>
<p id="p6">取消</p>
</div>
</body>
1
2
3
4
5
6
7
8
9
10
11
var body = document.body
bindEvent(body, 'click', function (e) {
// 所有 p 的点击都会冒泡到 body 上,因为 DOM 结构中 body 是 p 的上级节点,事件会沿着 DOM 树向上冒泡
alert('取消')
})

var p1 = document.getElementById('p1')
bindEvent(p1, 'click', function (e) {
e.stopPropagation() // 阻止冒泡
alert('激活')
})

事件代理

1
2
3
4
5
6
7
<div id="div1">
<a href="#">a1</a>
<a href="#">a2</a>
<a href="#">a3</a>
<a href="#">a4</a>
</div>
<button>点击增加一个 a 标签</button>
1
2
3
4
5
6
7
8
9
var div1 = document.getElementById('div1')
div1.addEventListener('click', function (e) {
// e.target 可以监听到触发点击事件的元素是哪一个
var target = e.target
if (target.nodeName === 'A') {
// 点击的是 <a> 元素
alert(target.innerHTML)
}
})
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
// 完善的通用事件绑定函数
function bindEvent(elem, type, selector, fn) {
// 这样处理,可接收两种调用方式 bindEvent(div1, 'click', 'a', function () {...}) 和 bindEvent(div1, 'click', function () {...}) 这两种
if (!fn) {
fn = selector
selector = null
}

// 绑定事件
elem.addEventListener(type, function (e) {
var target
if (selector) {
// 有 selector 说明需要做事件代理
// 获取触发时间的元素,即 e.target
target = e.target
// 看是否符合 selector 这个条件
if (target.matches(selector)) {
fn.call(target, e)
}
} else {
// 无 selector ,说明不需要事件代理
fn(e)
}
})
}
------------------------------
// 使用代理,bindEvent 多一个 'a' 参数
var div1 = document.getElementById('div1')
bindEvent(div1, 'click', 'a', function (e) {
console.log(this.innerHTML)
})

// 不使用代理
var a = document.getElementById('a1')
bindEvent(div1, 'click', function (e) {
console.log(a.innerHTML)
})

使用代理的优点如下:
使代码简洁
减少浏览器的内存占用

Ajax

XMLHttpRequest

1
2
3
4
5
6
7
8
9
10
11
var xhr = new XMLHttpRequest()
xhr.open("GET", "/api", false) // 最后一个参数async = true表示异步请求,false表示同步请求
xhr.onreadystatechange = function () {
// 这里的函数异步执行,可参考之前 JS 基础中的异步模块
if (xhr.readyState == 4) {
if (xhr.status == 200) {
alert(xhr.responseText)
}
}
}
xhr.send(null)

状态码说明
xhr.readyState是浏览器判断请求过程中各个阶段的,
xhr.status是 HTTP 协议中规定的不同结果的返回状态说明。

xhr.readyState的状态码说明

  • 0 - 初始状态
  • 1 - open()
  • 2 - send()
  • 3 - send()后接受到返回数据
  • 4 - send()后接受完返回数据

复习xhr.status

  • 200 正常
  • 3xx
  • 301 永久重定向。如http://xxx.com这个 GET 请求(最后没有/),就会被301到http://xxx.com/(最后是/)
  • 302 临时重定向。临时的,不是永久的
  • 304 资源找到但是不符合请求条件,不会返回任何主体。如发送 GET 请求时,head 中有If-Modified-Since: xxx(要求返回更新时间是xxx时间之后的资源),如果此时服务器 端资源未更新,则会返回304,即不符合要求
  • 404 找不到资源
  • 5xx 服务器端出错了

Fetch API

目前已经有一个获取 HTTP 请求更加方便的 API:Fetch,通过Fetch提供的fetch()这个全局函数方法可以很简单地发起异步请求,并且支持Promise的回调。但是 Fetch API 是比较新的 API,具体使用的时候还需要查查 caniuse,看下其浏览器兼容情况。

看一个简单的例子:

1
2
3
4
5
6
7
8
 fetch('some/api/data.json', {
method:'POST', //请求类型 GET、POST
headers:{}, // 请求的头信息,形式为 Headers 对象或 ByteString
body:{}, //请求发送的数据 blob、BufferSource、FormData、URLSearchParams(get 或head 方法中不能包含 body)
mode:'', //请求的模式,是否跨域等,如 cors、 no-cors 或 same-origin
credentials:'', //cookie 的跨域策略,如 omit、same-origin 或 include
cache:'', //请求的 cache 模式: default、no-store、reload、no-cache、 force-cache 或 only-if-cached
}).then(function(response) { ... });

手动封装一个兼容低版本的fetch: https://gitee.com/taoyingsong/open-skill/blob/master/fetch.js
更多关于fetch及promise的介绍:
https://github.com/camsong/blog/issues/2
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise

跨域

什么是跨域:相同的协议、域名、端口号即同源,否则就是跨域。

HTML 中几个标签能逃避过同源策略——<script src="xxx"><img src="xxxx"/><link href="xxxx">,这三个标签的src/href可以加载其他域的资源,不受同源策略限制。

因此:

  • <img>可以做打点统计,因为统计方并不一定是同域的,在讲解 JS 基础知识异步的时候有过代码示例。除了能跨域之外,<img>几乎没有浏览器兼容问题,它是一个非常古老的标签。

  • <script><link>可以使用 CDN,CDN 基本都是其他域的链接。

  • 另外<script>还可以实现 JSONP,能获取其他域接口的信息,接下来马上讲解。

注意,所有的跨域请求方式,最终都需要信息提供方来做出相应的支持和改动,也就是要经过信息提供方的同意才行,否则接收方是无法得到它们的信息的,浏览器是不允许的。

解决跨域 - JSONP

首先,有一个概念你要明白,例如访问http://coding.com/classindex.html的时候,服务器端就一定有一个classindex.html文件吗?—— 不一定,服务器可以拿到这个请求,动态生成一个文件,然后返回。 同理,<script src="http://coding.com/api.js">也不一定加载一个服务器端的静态文件,服务器也可以动态生成文件并返回。

首先,我们在自己的页面这样定义

1
2
3
4
5
6
<script>
window.callback = function (data) {
// 这是我们跨域得到信息
console.log(data)
}
</script>

请求<script src="http://coding.com/api.js">时,后台动态生成如下内容:

1
callback({x:100, y:200})

最后我们在页面中加入<script src="http://coding.com/api.js"></script>,那么这个js加载之后,就会执行内容,我们就得到内容了。

解决跨域 - CORS: 服务器端设置 http header

这是需要在服务器端设置的,作为前端工程师我们不用详细掌握,但是要知道有这么个解决方案。而且,现在推崇的跨域解决方案是这一种,比 JSONP 简单许多。

1
2
3
4
5
6
response.setHeader("Access-Control-Allow-Origin", "http://m.test.com/");  // 第二个参数填写允许跨域的域名称,不建议直接写 "*"
response.setHeader("Access-Control-Allow-Headers", "X-Requested-With");
response.setHeader("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");

// 接收跨域的cookie
response.setHeader("Access-Control-Allow-Credentials", "true");

比较:

  1. JSONP的主要优势在于对浏览器的支持较好;虽然目前主流浏览器支持CORS,但IE10以下不支持CORS。
  2. JSONP只能用于获取资源(即只读,类似于GET请求);CORS支持所有类型的HTTP请求,功能完善。(这点JSONP被玩虐,但大部分情况下GET已经能满足需求了)
  3. JSONP的错误处理机制并不完善,我们没办法进行错误处理;而CORS可以通过onerror事件监听错误,并且浏览器控制台会看到报错信息,利于排查。
  4. JSONP只会发一次请求;而对于复杂请求,CORS会发两次请求。
  5. 始终觉得安全性这个东西是相对的,没有绝对的安全,也做不到绝对的安全。毕竟JSONP并不是跨域规范,它存在很明显的安全问题:callback参数注入和资源访问授权设置。CORS好歹也算是个跨域规范,在资源访问授权方面进行了限制(Access-Control-Allow-Origin),而且标准浏览器都做了安全限制,比如拒绝手动设置origin字段,相对来说是安全了一点。但是回过头来看一下,就算是不安全的JSONP,我们依然可以在服务端端进行一些权限的限制,服务端和客户端也都依然可以做一些注入的安全处理,哪怕被攻克,它也只能读一些东西。就算是比较安全的CORS,同样可以在服务端设置出现漏洞或者不在浏览器的跨域限制环境下进行攻击,而且它不仅可以读,还可以写。

应用场景:
如果你需要兼容IE低版本浏览器,无疑,JSONP。
如果你需要对服务端资源进行谢操作,无疑,CORS。

其他跨域解决方案Frame代理、Comet、Web Socket等平时都没用过就不深究了。

存储

cookie、localStorage、sessionStorage

cookie
cookie 本身不是用来做服务器端存储的,它是设计用来在服务器和客户端进行信息传递的,因此我们的每个 HTTP 请求都带着 cookie。但是 cookie 也具备浏览器端存储的能力(例如记住用户名和密码),因此就被开发者用上了。
使用起来也非常简单,document.cookie = … 即可。
但是 cookie 有它致命的缺点:

  • 存储量太小,只有 4KB
  • 所有 HTTP 请求都带着,会影响获取资源的效率
  • API 简单,需要封装才能用
  • 一般由服务器生成,可设置失效时间。如果在浏览器端生成Cookie,默认是关闭浏览器后失效
  • 应用场景:比较常用的一个应用场景就是判断用户是否登录。针对登录过的用户,服务器端会在他登录时往 Cookie 中插入一段加密过的唯一辨识单一用户的辨识码,下次只要读取这个值就可以判断当前用户是否登录啦。

localStorage
它是专门为了浏览器端缓存而设计的。其优点有:

  • 存储量增大到 5MB
  • 不会带到 HTTP 请求中
  • API 适用于数据存储 localStorage.setItem(key, value) localStorage.getItem(key)。 (针对localStorage.setItem,使用时尽量加入到try-catch中,某些浏览器是禁用这个 API 的,要注意)
  • localStorage除非被清除,否则永久保存
  • 应用场景:管理购物车,保存HTML5游戏的一些本地数据

sessionStorage

  • 存储量增大到 5MB
  • 不会带到 HTTP 请求中
  • API 适用于数据存储
  • 仅在当前会话下有效,关闭页面或浏览器后被清除
  • 应用场景:内容特别多的表单,我们可能要把表单页面拆分成多个子页面,然后按步骤引导用户填写。这时候 sessionStorage 的作用就发挥出来了。或者刷新页面保留想某些效果。