除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正常3xx301永久重定向。如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 和知识。