浏览器模型相关

150 阅读8分钟

常见浏览器 JS 对象及常见 API 及用法

什么是浏览器对象模型

BOM: Browser Object Model (浏览器对象模型), 浏览器模型提供了独立于内容的、可以与浏览器窗口进行活动的对象结构,就是浏览器提供的 API 其主要对象有:

  1. window 对象 -- BOM 的核心,是 js 访问浏览器的接口,也是 ES 规定的 Global 对象
  2. location 对象:提供当前窗口中的加载的文档有关的信息和一些导航功能。即是 window 对象属性,也是 document 的对象属性 可以通过window.location 和 document.location 拿到
  3. navigation 对象:获取浏览器的系统信息
  4. screen 对象:用来表示浏览器窗口外部的显示器的信息等
  5. history 对象:保存用户上网的历史信息 (Vue history 路由模式基于这个对象) 访问A,让后访问B网页 -> 会存在 history 栈中1,2。 前进/返回

window 对象

window 对象是整个浏览器对象模型的核心,其扮演着既是接口又是全局对象的角色

alert()
confirm()
prompt()
此上三个都不太常用,都会自己实现

open() // 打开新页面

// 面试重点:前端监控,业务节点监控如下单、支付;是否有全局报错,入js报错,资源报错 onerror() 实现:addEventListener('error')

// 面试:用setTimeout实现setInterval

const mySetInterval = (callback, time) => {
    (function inner() {
        const timer = setTimeout(() => {
            callback()
            clearInterval(timer)
            inner()
        }, time)
    })() // setInterval 是自执行
}

// 用setInterval 实现 setTimeout

const mySetTimeout = (callback, time) {
  const timer = setInterval(() => {
      clearInterval(timer)
      callback();
  }, time)
}

setTimeout() setInterval()

  • 窗口位置

screenLeft // 适用于Safari, Chrome, IE
screenTop // 适用于Safari, Chrome, IE
screenX // 适用于Firefox
screenY // 适用于Firefox
moveBy(x,y) moveTo(x,y)

  • 窗口大小

常用于浏览器宽高比计算:

innerWidth // 可视窗口大小

innerHeight

兼容ie视窗大小

const clientWidth = window.innerWidth || document.body.clientWidth

outerWidth // 浏览器大小 outerHeight

resizeTo(width, height) resizeBy(width, height)

Location 对象

提供当前窗口中的加载的文档有关的信息和一些导航功能。即是 window 对象属性,也是 document 的对象属性

location 对象的主要属性:
hash <--> history 对象
-- 例子:"#host" -- 说明:返回url中的hash(#后字符>=0, 包含#的一部分)

host 区分测试环境或线上环境
-- 例子:"juejin.im:80" -- 说明:服务器名称 + 端口(如果有)

hostname
href
pathname 整个页面的路径
port
protocol
search 返回url的查询字符串,以问号开头,包含问号

location 的应用场景:讲课时补充

Navigation 对象

navigation 接口表示用户代理的情况和标示,允许脚本查询它和注册自己进行的一些活动

isOnline 监听网络连接情况

History 对象

history 对象保存着用户上网的历史记录,从窗口打开的那一刻算起,history 对象是用窗口的浏览历史用文档和文档状态列表的形式表示。

go() back() === go(-1) forward() === go(1) length 获取当前页面中有多少个站

事件代理捕获、冒泡

面试题

  1. 事件委托/事件代理

包含几个阶段

捕获阶段 -> 目标阶段 -> 冒泡阶段

捕获阶段: 从 window 开始一步步往下捕获到事件元素
冒泡阶段:再返回上去
🌰: input

window -> body -> input -> body -> window

window.addEventListener('click', function(e) {
  console.log(`widow 捕获`, e.target.nodeName, e.currentTarget.nodeName)
}, true)
// 第三个参数传入 true 为捕获,不传/ false 为冒泡

面试问题:e.target.nodeName, e.currentTarget.nodeName有什么区别?

e.target.nodeName: 指当前点击的元素。 // 触发事件的元素

e.currentTarget.nodeName: 指绑定事件监听的元素

<div>
  <button>click</button>
</div>
window.addEventListener('click', (e) => {
      console.log('e.target', e.target) // 如果点击button 则为button,点击div 则为div
      console.log('e.currentTarget', e.currentTarget) // 点啥都是 window
    }, true)

第三个参数

如何在事件捕获和冒泡中,区分捕获阶段还是冒泡阶段?

通过 addEventListener 的第三个参数:传入 true 为捕获,不传/ false 为冒泡

阻止事件的传播

e.stopPropagation(); // 阻止事件冒泡? 错!!

阻止事件传播:不止有捕获还有冒泡

场景设计题

现在有一个页面,这个页面上有许多元素,div p button 每个元素上都有自己的 click 事件,都不相同。

现在来了一个新的需求:一个用户进入这个页面的时候,会有一个状态 banned, window.banned

true: 当前用户被封禁了,用户点击当前页面的任何元素,都不执行 click 逻辑,而是 alert 弹窗,提示你被封禁了! false: 不做任何操作

  1. 在最顶级阻止事件传播
window.addEventListener('click', function(e) {
  if (banned) {
    e.stopPropagation();
    alert('你被封禁了!');
    return;
  }
  console.log(`window 捕获`, e.target.nodeName, e.currentTarget.nodeName);
  // e.currentTarget 为事件监听的元素
  // e.target 为点击事件的元素
}, true) // 第三个参数为捕获阶段
  1. 也可以在最上层设置一个遮罩层

阻止默认行为

e.preventDefault(); // a标签跳转其他页面 || 拖拽图片到浏览器,直接打开图片 || 点击表单提交按钮,提交表单

// 拦截a标签跳百度
const baidu = document.getElementById('a-baidu');

baidu.addEventListener('click', function(e) {
  e.preventDefault();
})

兼容性

addEventListener - firefox chrome ie高版本 safari opera
attachEvent - ie7 ie8 // 不兼容事件 捕获

兼容一个addEventListener

class BomEvent {
  constructor(element) {
    this.element = element;
  }

  addEvent(type, handler) {
    if (this.element.addEventListener) {
      this.element.addEventListener(type, handler, false); // ie不支持捕获 
    } else if (this.element.attachEvent) {
      this.element.attachEvent(`on${type}`, handler);
    } else {
      this.element[`on${type}`] = handler;
    }
  }

  removeEvent(type, handler) {
    if (this.element.removeEventListener) {
      this.element.removeEventListener(type, handler, false);
    } else if (this.element.detachEvent) {
      this.element.detachEvent(`on${type}`, handler);
    } else {
      this.element[`on${type}`] = null;
    }
  }
}

兼容事件冒泡

function stopPropagation(ev) {
  if (ev.stopPropagation) {
    ev.stopPropagation(); // 标注e3c
  } else {
    ev.cancelBubble = true; // ie
  }
}

兼容防止默认事件

function preventDefault(event) {
  if (event.preventDefault) {
    event.preventDefault();
  } else {
    event.returnValue = false;
  }
}

事件委托/代理

将子元素的事件委托到父元素上

注意这里有坑:

  1. 部分浏览器 tagName 会输出大写
  2. querySelectorAll 返回的是一个伪数组,并不可以用 indexOf 方法
  1. 使用原型链改变this指向 Array.prototype.indexOf.call(liList, target)
  2. 使用Array.from(liList) 转成数组
// 暴力方法
            // const liList = document.getElementsByTagName('li');
            // for (let i = 0; i < liList.length; i++) {
            //   liList[i].addEventListener('click', function(e) {
            //     alert(`内容为${e.target.innerHTML}, 索引为${i}`)
            //   }) 
            // }

          // 该方式性能很差,因此应该使用代理的方式
          const ul = document.querySelector('ul');

          ul.addEventListener('click', function(e) {
            const target = e.target;
            // 点击为li的元素,注意某些浏览器会返回大写
            if (target.nodeName.toLowerCase() === 'li') {
              const liList = this.querySelectorAll('li');
              // 方式一:const index = Array.prototype.indexOf.call(liList, target);
              // 方式二:
              const realList = Array.from(liList);
              const index = realList.indexOf(target);

              alert(`内容为${target.innerHTML}, 索引为${index}`)
            }
          })

ajax 及 fetch API 详解

  1. XMLHTTPRequest
const xhr = new XMLHttpRequest();

xhr.open('GET', 'http://domain/serve');

xhr.onreadystatechange = function() {
  if (xhr.readyState !== 4) {
    return;
  }

  if (xhr.status === 200) {
    console.log(xhr.responseText);
  } else {
    console.error(`HTTP error, status=${xrh.status}, errorText=${xrh.statusText}`);
  }
}

readyState 状态码:

0 - (未初始化)还没有调用send()方法
1 - (载入)已调用send()方法,正在发送请求
2 - (载入完成)send()方法执行完成,已经接收到全部响应内容
3 - (交互)正在解析响应内容
4 - (完成)响应内容解析完成,可以在客户端调用了

注意:

  1. send 在状态监听之后发送,因为如果请求响应很快,得不到任何反馈
  2. 处理超时:
// 处理请求超时
  xhr.timeout = 3000;
  xhr.ontimeout = () => {
    console.error('当前请求超时!');
  }
  1. 长时间文件上传文件进度:
xhr.upload.onprogress = p => {
  const percentage = Math.round((p.loaded / p.total) * 100) + '%';
}
  1. fetch 内部封装了promise
fetch('http://domain/serviece', {
  method: 'GET',
  credentials: 'same-origin' // 同域请求会携带cookie
}).then(response => {
  if (response.ok) {
    // 请求成功
    return response.json();
  }
  throw new Error('http error');
}).then(json => {
  console.log(json);
}).catch(error => {
  // 整体错误的管理
  console.error(error);
})

fetch 设置超时:本身不支持 封装一个timeout

function fetchTimeout(url, init, timeout = 3000) {
  return new Promise((resolve, reject) => {
    // 判断逻辑是内部构造函数几乎同步执行,fetch和setTimeout同步执行;如果fetch 100ms 执行完毕,.then调用resolve改变整个promise 状态为fulfilled,3000 ms后无法再改promise 状态,第二行没有意义;但超时如10000ms 之后,setTimeout先被执行了,改变了promise 的状态为rejected,第一行代码没用了 
    fetch(url, init).then(resolve).catch(reject);
    setTimeout(reject, timeout);
  })
}

// 课后小作业: // 尝试封装一个通用的异步函数超时逻辑

function xx(fn, timeout)

中断fetch()

const controller = new AbortController();

fetch('http://domain/serviece', {
  method: 'GET',
  credentials: 'same-origin', // 同域请求会携带cookie
  signal: controller.signal
}).then(response => {
  if (response.ok) {
    // 请求成功
    return response.json();
  }
  throw new Error('http error');
}).then(json => {
  console.log(json);
}).catch(error => {
  // 整体错误的管理
  console.error(error);
})

controller.abort();

请求头

method path

cookie 标示了用户信息 问题:为什么常见的 cdn 域名 和 业务域名不一样? 如:www.baidu.com 业务域名 cdn.baidu-aa.com cdn 域名

  1. 安全问题:浏览器在发送 cdn 请求会带上用户的 cookie 信息,而业务上不想暴露用户信息给 cdn 厂商
  2. cdn 的用途是提升性能,而每次 request header 都会携带 cookie,无谓增加了带宽和流量消耗
  3. http 1.1 绕过并发请求数,达到最大限制数; http 2.0没有限制

referer: 标识访问路径,当前来自于哪个页面 如来自于百度 user-agent: 判断各种环境

response header

access-control-allow-origin 限制请求域名

access-control-allow-origin: http:www.baidu.com
// 不限制
access-control-allow-origin: \*

content-encoding: gizp 资源打包到 cdn 进行压缩 set-cookie: :value [ ;expires=date][ ;domain=domain][ ;path=path][ ;secure] etag last-modified age

status

200 GET 请求成功
201 POST 请求成功
301 永久重定向
302 临时重定向
304 协商缓存,服务器文件未修改

强缓存:通过 max-age expired 来标识
expired 弊端:2012xxx 服务器事件改变会产生偏差
max-age:1000 接受到cookie 1000ms之后就失效了

协商缓存:浏览器和服务器协商,是否需要缓存
last-modified 上次修改时间。通过上次修改进行判断
弊端:打开之后没有改动,也会更改last-modified
etag: 整体文件内容进行hash,通过diff 判断是否修改
弊端:耗性能

面试题:vue/react 常见的spa 都会存在一个index.html 文件,这个也是所谓的单页。针对index.html 文件,如果要做缓存,适合做什么样的缓存?

从index.html 文件特性入手:本身并没有什么内容,但是编译完成后会产出各种js文件 css文件,变成script link 标签插入到 index.html
插入的js css 文件有 hash 命名 -> 防止缓存,但index.html 没有 hash 的
js文件更改的非常快,如果index.html 做了强缓存,一天之后才能够更新,如果其中的script 有bug,不论怎么更新,都是原来的script。如果做了协商缓存,可以随时更新。文件因此需要协商缓存 通常index.html 不做缓存