除ES基础之外,Web前端经常会⽤到⼀些跟浏览器相关的API
知识点梳理
- BOM操作
- DOM操作
- 事件绑定
- Ajax
- 存储
BOM
BOM(浏览器对象模型)是浏览器本身的⼀些信息的设置和获取,例如获取浏览器的宽度、高度,设置让浏览器跳转到哪个地址。
- navigator
- screen
- location
- history
这些对象就是⼀堆⾮常简单粗暴的API,去MDN或者w3school这种⽹站⼀查就都明⽩了。下⾯列举⼀下常⽤功能的代码示例获取浏览器特性(即俗称的UA
)然后识别客户端,例如判断是不是Chrome浏览器。
获取浏览器特性(即俗称的UA)然后识别客户端,例如判断是不是Chrome浏览器。
var ua = navigator.userAgent
var isChrome = ua.indexOf('Chrome')
console.log(isChrome)
获取屏幕的宽度和高度:
console.log(screen.width)
console.log(screen.height)
获取⽹址、协议、path、参数、hash等:
//例如当前⽹址是 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.search) // ?a=10&b=10
console.log(location.hash) // #some
另外,还有浏览器的前进、后退功能等:
history.back()
history.forward()
DOM
题⽬:DOM和HTML区别和联系
什么是DOM
讲DOM先从HTML讲起,讲HTML
先从XML
讲起。XML是⼀种可扩展的标记语⾔,所谓可扩展就是它可以描述任何结构化的数据,它是⼀棵树!
<?xml version="1.0" encoding="UTF-8" ?>
<note>
<to>Tove</to>
<from>Jani</from>
<heading>Reminder</heading>
<body>Don't forget me this weekend!</body>
<other>
<a></a>
<b></b>
</other>
</note>
HTML是⼀个有既定标签标准的XML格式,标签的名字、层级关系和属性,都被标准化(否则浏览器⽆法解析)。同样,它也是⼀棵树。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div>
<p>this is p</p>
</div>
</body>
</html>
开发完的HTML代码会保存到⼀个⽂档中(⼀般以.html
或者.htm
结尾),⽂档放在服务器上,浏览器请求服务器,这个⽂档被返回。因此,最终浏览器拿到的是⼀个⽂档⽽已,⽂档的内容就是HTML格式的代码。
但是浏览器要把这个⽂档中的HTML按照标准渲染成⼀个⻚⾯,此时浏览器就需要将这堆代码处理成⾃⼰能理解的东⻄,也得处理成JS能理解的东⻄,因为还得允许JS修改⻚⾯内容呢。
基于以上需求,浏览器就需要把HTML
转变成DOM
,HTML是⼀棵树,DOM也是⼀棵树。对DOM的理解,可以暂时先抛开浏览器的内部因素,先从JS着⼿,即可以认为DOM就是JS能识别的HTML结构,⼀个普通的JS对象或者数组。
获取DMO节点
最常⽤的DOM API就是获取节点,其中常⽤的获取⽅法如下⾯代码示例:
// 通过 id 获取
var div1 = document.getElementsById('div1') // 元素
// 通过 tagname 获取
var divList = document.getElementsByClassName('div') // 集合
console.log(divList.length)
console.log(divList[0])
// 获取 class 获取
var containerList = document.getElementsByClassName('containerList') // 集合
// 通过 CSS 选择期获取
var pList = document.querySelectorAll('p') // 集合
题目:property和attribute的区别是什么?
property
DOM节点就是⼀个JS对象,它符合之前讲述的对象的特征——可扩展属性,因为DOM节点本质上也是⼀个JS对象。因此,如下代码所示,p
可以有style
属性,有classNamenodeNamenodeType
属性。注意,这些都是JS范畴的属性,符合JS语法标准的。
var pList = document.querySelectorAll('p')
var p = pList[0]
console.log(p.style.width) // 修改样式
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没有关系。
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有哪些?
DOM树操作
新增节点
var div1 = document.getElementById('div1')
// 新增新节点
var p1 = document.createElement('p')
p1.innerHTML = 'this is p1'
div1.appendChild(p1) //添加新创建的元素
// 移动已有的父节点。注意,这里的"移动",并不是拷贝
var p2 = document.getElementById('p2')
div1.appendChild(p2)
获取父节点
var div1 = document.getElementById('div1')
var parent = document.parentElement
获取子节点
var div1 = document.getElementById('div1')
var child = div1.childNodes
删除节点
var div1 = document.getElementById('div1')
var child = div1.childNodes
div1.removeChild(child[0])
还有其他操作的API,例如获取前⼀个节点、获取后一个节点等.
事件
事件绑定
普通的事件绑定写法如下:
var btn = document.getElementById('btn')
btn.addEventListener('click', function (event) {
// event.preventDefault() // 阻止默认行为
// event.stopPropagation() // 阻止冒泡
console.log('clicked');
})
为了编写简单的事件绑定,可以编写通⽤的事件绑定函数。这里虽然比较简单,后面再完善。
// 通用的事件绑定函数
function bindEvent(el, type, fn) {
el.addEventLister(type, fn)
}
var a = document.getElementById('link')
// 写起来更加简单了
bindEvent(a, 'click', function(e) {
e.preventDefault();
alter('clicked')
})
题目:什么是事件冒泡?
<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>
对于以上HTML代码结构,要求点击p1
时候进激活状态,点击其他任何<p>
都取消激活状态,如何实现?代码如下,注意看注释:
var body = document.body
bindEvent(body, 'click', function (e) {
// 所有p的点击都会冒泡到body上, 因为DOM结构中body是p的上级节点
// 事件会沿着DOM树向上冒泡
alert('取消')
})
var p1 = document.getElementById('p1')
bindEvent(body, 'click', function (e) {
e.stopPropagation(); // 阻止冒泡
alert('激活')
})
如果我们在p1
div1
body
中都绑定了事件,它是会根据DOM的结构来冒泡,从下到上挨个执行e.stopPropagation()
就可以阻止冒泡。
题目:如何使⽤事件代理?有何好处?
事件代理
设定一种场景,如下代码,一个<div>
中包含了若干个<a>
,⽽且还能继续增加。那如何快捷⽅便地为所有<a>
绑定事件呢?
<div id="div1">
<a href="#">a1</a>
<a href="#">a2</a>
<a href="#">a3</a>
<a href="#">a4</a>
</div>
<button>点击添加一个 a 标签</button>
这里就会用到事件代理。我们要监听<a>
的事件,但要把具体的事件绑定到<div>
上,然后看事件的触发点是不是<a>
。
var div1 = document.getElementById('div1')
div1.addEventListener('click', function (e) {
// e.target 可以监听到出发点击事件的元素是哪一个
var target = e.target
if (e.nodeName === 'A') {
// 点击的是 <a> 元素
alert(target.innerHTML)
}
})
现在完善一下之前写的通⽤事件绑定函数,加上事件代理。
function bindEvent(elem, type, selector, fn) {
if (fn == null) {
fn = selector
selector = null
}
// 绑定事件
elem.addEventListener(type, function(e) {
var target
if (selector) {
// 有 slector 说明需要做的事件代理
// 获取触发时间的元素,即 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('a')
bindEvent(div1, 'click', function(e) {
console.log(this.innerHTML);
})
最后,使用代理的优点如下:
- 使代码简洁
- 减少浏览器的内存占用
Ajax
题目:手写XMLHttpRequest不借助任何库
var xhr = new XMLHttpRequest()
xhr.onreadystatechange = function() {
// 这里的函数异步执行
if (xhr.readyState == 4) {
if (xhr.status == 200) {
alert(xhr.responseText)
}
}
}
xhr.open('GET', '/api', false)
xhr.send(null)
当然,使用jQuery
、Zepto
或Fetch
等库来写就更加简单了。
状态码说明
上述代码中,有两处状态码需要说明。xhr.readyState
是浏览器判断请求过程中各个阶段的,xhr.status
是HTTP协议中规定的不同结果的返回状态说明。
xhr.readyState
的状态码说明:
- 0-代理被创建,但尚未调用open()方法。
- 1 -
open()
方法已经被调用。 - 2 -
send()
方法已经被调用,并且头部和状态已经可获得。 - 3 - 下载中,
responseText
属性已经包含部分数据。 - 4 - 下载操作已完成
题目:HTTP协议中,response的状态码,常见的有哪些?
xhr.status
即HTTP状态码,有2xx
3xx
4xx
5xx
这几种,比较常见的有以下几种:
200
正常3xx
301
永久重定向。如http://xxx.com
这个GET请求(最后没有/
),就会被301到http://xxx.com/
(最后是/
)302
临时重定向。临时的,不是永久的304
资源找到但是不符合请求条件,不会返回任何主体。如发送GET请求时,head中有If-Modified-Since: xxx(要求返回更新时间是xxx时间之后的资源),如果此时服务器端资源未更新,则会返回304,即不符合要求
404
找不到资源5xx
服务器端出错了
理解为何上述代码中要同时满足xhr.readyState == 4
和xhr.status == 200
。
Fetch API
⽬前已经有个获取HTTP请求更加⽅便的API:Fetch
,通过Fetch提供的fetch()
这个全局函数⽅法可以很简单地发起异步请求,并且支持Promise
的回调。但是Fetch API是比较新的API,具体使⽤的时候还需要查查caniuse
,看下其浏览器兼容情况。
fetch('some/api/data.json', {
method: 'POST',
headers: {},
body: {},
mode: '',
credentials:'',
cache: '',
}).then(res => {})
Fetch
支持headers
定义,通过headers⾃定义可以⽅便地实现多种请求⽅法(PUT、GET、POST等)、请求头(包括跨域)和cache
策略等;除此之外还⽀持response(返回数据)多种类型,比如支持二进制文件、字符串和formData
等。
跨域
题目:如何实现跨域?
浏览器中有同源策略,即一个域下的页面中,无法通过Ajax获取到其他域的接口。例如有一个接口http://m.juejin.com/course/ajaxcourserecom?cid=459
,你自己的个页面http://www.yourname.com/page1.html
中的Ajax无法获取这个接口。这正是命中了“同源策略”。如果浏览器哪些地方忽略了同源策略,那就是浏览器的安全漏洞,需要紧急修复。
url哪些地⽅不同算作跨域?
- 协议
- 域名
- 端口
但是HTML
中几个标签能逃避过同源策略——<script src="xxx">
、<imgsrc="xxxx"/>
、<link href="xxxx">
,这三个标签的src/href
可以加载其他域的资源,不受同源策略限制。因此,这使得这三个标签可以做⼀些特殊的事情。<img>
可以做打点统计,
除了能跨域之外,几乎没有浏览器兼容问题,它是一个非常古老的标签。
<script>
和<link>
可以使用CDN
,CDN基本都是其他域的链接。另外<script>
还可以实现JSONP
,能获取其他域接口的信息。
但是请注意,所有的跨域请求方式,最终都需要信息提供⽅来做出相应的⽀持和改动,也就是要经过信息提供方的同意才行,否则接收方是无法得到它们的信息的,浏览器是不允许的。
解决跨域- JSONP
首先,有一个概念要明白,例如访问http://coding.m.juejin.com/classindex.html
的时候,服务器端就一定有一个classindex.html
文件吗?——不一定,服务器可以拿到这个请求,动态生成一个文件,然后返回。同理,<script src="http://coding.m.juejin.com/api.js"></script>
也不一定加载一个服务器端的静态文件,服务器也可以动态⽣成⽂件并返回。OK,接下来正式开始。例如我们的网站和掘金网,肯定不是这个域。我们需要掘金网提供一个接口,供我们来获取。首先,我们在自己的页面这样定义。
<script>
window.callback = function(data) {
//这是跨域得到的数据
console.log(data);
}
</script>
我们在⾃⼰的⻚⾯这样定义 然后掘⾦⽹给我提供了⼀个 http://coding.m.juejin.com/api.js
, 内容如下 (之前说过, 服务 器可动态⽣成内容)
callback({x: 100, y:200})
最后我们在⻚⾯中加⼊ <script src="http://coding.m.juejin.com/api.js"></script>
, 那 么这个 js 加载之后, 就会执⾏内容, 我们就得到内容了。
解决跨域 - 服务器端设置 http header
这是需要在服务器端设置的,⽽且,现在推崇的跨域解决⽅案是这⼀种, ⽐JSONP简单许多。
response.setHeader("Access-Control-Allow-Origin", "http://m.juejin.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");
存储
题⽬: cookie 和 localStorage 有何区别?
cookie 本身不是⽤来做服务器端存储的(计算机领域有很多这种 “ 狗拿耗⼦ ” 的例⼦, 例如 CSS 中的 float ), 它是设计⽤来在服务器和客户端进⾏信息传递的, 因此我们的每个 HTTP 请求都带着 cookie 。 但是 cookie 也具备浏览器端存储的能⼒ (例如记住⽤户名和密码),因此就被开发者⽤上了。
使⽤起来也⾮常简单,document.cookie = . . . .
即可。
但是 cookie 有它致命的缺点:
- 存储量太⼩,只有 4KB
- 所有HTTP请求都带着,会影响获取资源的效率
- API简单,需要封装才能⽤
localStroage 和 sessionStorage
后来,HTML5 标准就带来了 sessionStorage
和 localStorage
, 先拿 localStorage 来说, 它 是专⻔为了浏览器端缓存⽽设计的。其优点有:
- 存储量增⼤到5MB
- 不会带到 HTTP 请求中
- API 适⽤于数据存储
localStorage.setItem(key, value)
localStorage.getItem(key)
sessionStorage
的区别就在于它是根据 session
过去时间⽽实现, ⽽ localStorage 会永久有 效, 应⽤场景不同。例如, ⼀些需要及时失效的重要信息放在 sessionStorage 中, ⼀些不重要但 是不经常设置的信息, 放在 localStorage 中。
另外有个⼩技巧, 针对 localStorage.setItem
, 使⽤时尽量加⼊到 try-catch
中,某些浏览器是禁⽤这个API的, 要注意。
小结
⽇常开发中最常⽤的 API 和知识。