高频前端面试题汇总之浏览器原理篇

3,471 阅读35分钟

一、浏览器安全

1.  什么是 XSS 攻击?

XSS 攻击指的是跨站脚本攻击,攻击者通过在网站注入恶意脚本,使之在用户的浏览器上运行,从而盗取用户的信息。其本质是因为网站没有对恶意代码进行过滤,与正常的代码混合在一起了,浏览器没有办法分辨哪些脚本是可信的,从而导致了恶意代码的执行。

攻击者可以通过这种攻击方式进行以下操作:

  • 获取页面的数据,如DOM、cookie
  • 占用服务器资源,从而使用户无法访问服务器
  • 破坏页面结构
  • 流量劫持(将链接指向某网站)

XSS 可以分为存储型、反射型和 DOM 型:

  • 存储型指的是恶意脚本会存储在服务器上,当浏览器请求数据时,脚本从服务器传回并执行。
  • 反射型指的是攻击者诱导用户访问一个带有恶意代码的 URL 后,服务器端接收数据处理后把带有恶意代码的数据发送到浏览器端,浏览器端解析这段带有 XSS 代码的数据后当做脚本执行。 
  • DOM 型指的通过修改页面的 DOM 节点形成的 XSS。

2. 如何防御 XSS 攻击?

  • cookie 使用 http-only,使得脚本无法获取
  • 对 v-html 和 innerHTML 加载的信息进行转义
  • 使用 CSP ,CSP 的本质是建立一个白名单,告诉浏览器哪些外部资源可以加载和执行。通常有两种方式来开启 CSP,一种是设置 HTTP 首部中的 Content-Security-Policy,一种是设置 meta 标签的方式。

3. 什么是 CSRF 攻击?

CSRF 攻击指的是跨站请求伪造攻击,攻击者诱导用户进入一个第三方网站,然后该网站向被攻击网站发送跨站请求。如果用户在被攻击网站中保存了登录状态,那么攻击者就可以利用这个登录状态,绕过后台的用户验证,冒充用户向服务器执行一些操作。

CSRF 攻击的本质是利用 cookie 会在同源请求中携带发送给服务器的特点,以此来实现用户的冒充。

常见的 CSRF 攻击有三种:

  • GET 类型的 CSRF 攻击,比如在网站中的一个 img 标签里构建一个请求,当用户打开这个网站的时候就会自动发起提交。
  • POST 类型的 CSRF 攻击,比如构建一个表单,然后隐藏它,当用户进入页面时,自动提交这个表单。
  • 链接类型的 CSRF 攻击,比如在 a 标签的 href 属性里构建一个请求,然后诱导用户去点击。

4. 如何防御 CSRF 攻击?

  • 进行同源检测:服务器根据 http 请求头中 origin 或者 referer 信息来判断请求是否为允许访问的站点,从而对请求进行过滤。这种方式的缺点是有些情况下 referer 可以被伪造,同时还会把搜索引擎的链接也给屏蔽了。
  • 使用 CSRF Token 进行验证:服务器向用户返回一个随机数 Token ,当网站再次发起请求时,在请求参数中加入服务器端返回的 token ,然后服务器对这个 token 进行验证。但是我们需要给网站中的所有请求都添加上这个 token,操作比较繁琐。
  • 对 Cookie 进行双重验证:服务器在用户访问网站页面时,向请求域名注入一个Cookie,内容为随机字符串,然后当用户再次向服务器发送请求的时候,从 cookie 中取出这个字符串,添加到 URL 参数中,然后服务器通过对 cookie 中的数据和参数中的数据进行比较,来进行验证。的,那么这种方式会失效。同时这种方式不能做到子域名的隔离。
  • 在设置 cookie 属性的时候设置 Samesite ,限制 cookie 不能作为被第三方使用用。

5. 什么是中间人攻击?

是指攻击者与通讯的两端分别创建独立的联系, 并交换其所收到的数据, 使通讯的两端认为他们正在通过⼀个私密的连接与对方直接对话, 但事实上整个会话都被攻击者完全控制,攻击者可以拦截通讯双⽅的通话并插⼊新的内容。

攻击过程如下:

  • 客户端发送请求到服务端,请求被中间⼈截获
  • 服务器向客户端发送公钥
  • 中间⼈截获公钥,保留在自己手上。然后自己⽣成⼀个伪造的公钥,发给客户端
  • 客户端收到伪造的公钥后,⽣成加密hash值发给服务器
  • 中间⼈获得加密hash值,⽤自己的私钥解密获得真秘钥,同时⽣成假的加密hash值,发给服务器
  • 服务器⽤私钥解密获得假密钥,然后加密数据传输给客户端

6. 如何防范中间人攻击?

  • 采用传输加密:SSL 和 TLS 可以阻止攻击者使用和分析网络流量
  • 安装 DAM 数据库活动监控:DAM 可以监控数据库活动,检测篡改数据

7. 有哪些可能引起前端安全的问题?

  • 跨站脚本攻击 (XSS)
  • 跨站请求伪造攻击(CSRF)
  • 恶意第三⽅库
  • iframe的滥⽤

8. 网络劫持有哪几种,如何防范?

  • DNS劫持: (输⼊京东被强制跳转到淘宝这就属于dns劫持)

  • HTTP劫持: (访问⾕歌但是⼀直有贪玩蓝⽉的⼴告),由于http明⽂传输,运营商会修改你的http响应内容(即加⼴告)

DNS劫持由于涉嫌违法,已经被监管起来,现在很少会有DNS劫持,⽽http劫持依然⾮常盛⾏,最有效的办法就是全站HTTPS,将HTTP加密,这使得运营商⽆法获取明⽂,就⽆法劫持你的响应内容。

二、浏览器缓存

1. Web缓存

image.png

2. 强缓存

如果浏览器判断所请求的目标资源有效命中,则直接从强制缓存中返回,无须与服务器进行通信。

其中与强制缓存相关的两个字段是expires和cache-control,expires是在HTTP1.0协议中声明的用来控制缓存失效日期时间戳的字段。若之后浏览器再次发起相同的资源请求,便会对比expires与本地当前的时间戳,如果当前请求的本地时间戳小于expires的值,则说明浏览器缓存的响应还未过期,可以直接使用而无须向服务器端再次发起请求。

由于expires对本地时间戳过分依赖,如果客户端的时间与服务器端的时间不同步,或者客户端的时间被篡改,那么对于缓存过期的判断可能就无法和预期相符。

为了解决expires判断的局限性,从HTTP1.1协议开始新增了cache-control字段进行完善。cache-control通过设置max-age来控制响应资源的有效期,它是一个以秒为单位的时间长度,如此便可避免服务器端和客户端时间戳不同步而造成的问题。如果Cache-Control的max-age和expires同时存在,则以max-age为准,Cache-Control的优先级更高。

Cache-Control的一些参数:
1.no-cache:强制进行协商缓存
2.no-store:禁止使用任何缓存
3.public:表示响应资源既可以被浏览器缓存,又可以被代理服务器缓存
4.private:响应资源只能被浏览器缓存,默认值
5.max-age:过期时长
6.s-maxage:表示缓存在代理服务器中的过期时长

由此可见,cache-control能够作为expires的完全替代方案,在项目实践中使用它就足够了,目前expires还存在的唯一理由就是向下兼容。

3. 协商缓存?

在使用本地缓存之前,需要向服务器发起一次GET请求,与之协商当前浏览器保存的本地缓存是否已经过期。通常是采用所请求资源的最近一次的修改时间戳来判断的。

(1)Last-Modified协商缓存流程

客户端第一次请求目标资源时,服务器返回的响应头包含last-modified和该资源的最后一次修改的时间戳,以及cache-control:no-cache,当客户端再次请求该资源的时候,会携带一个if-modified-since字段,将这个字段对应的时间与目标资源的时间戳进行对比,如果没有变化则返回一个304状态码。

需要注意的是:协商缓存判断缓存有效的响应状态码是304,强制缓存判断有效的话,响应状态码是200

(2)last-modified 的不足

last-modified是根据请求资源最后的修改时间戳进行判断的,但是有时候我们对请求的文件资源进行了编辑,但是内容并没有发生任何变化,此时时间戳也会更新,需要重新进行完整的资源请求,这会造成网络带宽资源的浪费。

文件资源修改的时间戳单位是秒,如果文件修改的速度非常快,假设在几百毫秒内完成,此时是无法识别出该文件资源是否更新。

(3)Etag协商缓存的流程

由于这些不足,在HTTP1.1协议中增加了 Etag 字段进行完善,它会根据文件资源的内容进行哈希计算生成一个字符串,如果文件资源的内容发生改变,那么会被感知到。

首先,服务端将要返回给客户端的资源计算生成一个字符串;接着检测客户端的请求标头中的if-None-Match字段的值和第一步计算的值是否一致,一致则返回304;如果不一致则返回etag标头和Cache-Control:no-cache

(4)Etag的不足

服务器对于生成文件资源的Etag需要付出额外的计算开销,如果资源的尺寸比较大,数量较多且修改频繁,那么生成的Etag的过程就会影响服务器的性能。

4. 点击刷新按钮或者按 F5、按 Ctrl+F5 (强制刷新)、地址栏回车有什么区别?

  • 点击刷新按钮或者按 F5: 浏览器直接对本地的缓存文件过期,但是会带上If-Modifed-Since,If-None-Match,这就意味着服务器会对文件检查新鲜度,返回结果可能是 304,也有可能是 200。
  • 用户按 Ctrl+F5(强制刷新): 浏览器不仅会对本地文件过期,而且不会带上 If-Modifed-Since,If-None-Match,相当于之前从来没有请求过,返回结果是 200。
  • 地址栏回车: 浏览器发起请求,按照正常流程,本地检查是否过期,然后服务器检查新鲜度,最后返回内容。

三、浏览器渲染原理

1. 浏览器的渲染过程

  • 首先解析收到的文档,根据文档定义构建一棵 DOM 树
  • 然后对 CSS 进行解析,生成 CSSOM 规则树
  • 根据 DOM 树和 CSSOM 规则树构建渲染树。渲染树的节点被称为渲染对象,渲染对象是一个包含有颜色和大小等属性的矩形,渲染对象和 DOM 元素相对应,但这种对应关系不是一对一的,不可见的 DOM 元素不会被插入渲染树。
  • 当渲染对象被创建并添加到树中,它们并没有位置和大小,所以当浏览器生成渲染树以后,就会根据渲染树来进行布局。
  • 布局阶段结束后是绘制阶段,遍历渲染树并调用渲染对象的 paint 方法将它们的内容显示在屏幕上,绘制使用 UI 基础组件。

2. 浏览器渲染优化

(1)针对JavaScript

1.JavaScript文件放在body的最后
2.使用async、defer来异步引入

(2)针对CSS

1.导入外部样式使用link,而不用@import
2.如果css少,尽可能采用内嵌样式,直接写在style标签中

(3)针对DOM树、CSSOM树

1.HTML文件的代码层级尽量不要太深
2.使用语义化的标签,来避免不标准语义化的特殊处理
3.减少CSSD代码的层级,因为选择器是从右向左进行解析的

(4)减少回流与重绘

1.不要使用table布局, 一个小的改动可能会使整个table进行重新布局
2.不要频繁操作元素的样式
3.DOM的多个读写操作放在一起
4.操作DOM时,尽量在低层级的DOM节点进行操作
5.避免频繁操作DOM,可以创建一个文档片段documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中

3. 渲染过程中遇到 JS 文件如何处理?

JavaScript 的加载、解析与执行会阻塞文档的解析,在构建 DOM 时,HTML 解析器若遇到了 JavaScript,那么它会暂停文档的解析,将控制权移交给 JavaScript 引擎,等 JavaScript 引擎运行完毕,浏览器再从中断的地方恢复继续解析文档。

4. 什么是文档的预解析?

当执行 JavaScript 脚本时,另一个线程解析剩下的文档,并加载后面需要通过网络加载的资源。这种方式可以使资源并行加载从而使整体速度更快。

5. CSS 如何阻塞文档解析?

理论上,既然样式表不改变 DOM 树,也就没有必要停下文档的解析等待它们。然而,JavaScript 脚本执行时可能在文档的解析过程中请求样式信息,如果样式还没有加载和解析,脚本将得到错误的值,显然这将会导致很多问题。所以如果浏览器尚未完成 CSSOM 的下载和构建,而我们却想在此时运行脚本,那么浏览器将延迟 JavaScript 脚本执行和文档的解析,直至其完成 CSSOM 的下载和构建。

四、浏览器本地存储

1. 浏览器本地存储方式

(1)Cookie

1.大小只有4kb,每次发起HTTP请求都会携带
1.Cookie一旦创建成功,名称就无法修改,无法跨域名
3.每个域名下Cookie的数量不能超过204.有安全问题,如果Cookie被拦截,那就可获得session的信息
5.可以设置过期时间
6.可以设置很多字段

(2)LocalStorage

优点:
1.大小一般为5MB
2.持久储存,除非主动清理,不然会永久存在
3.仅储存在本地,不像Cookie那样每次HTTP请求都会被携带

缺点:
1.存在浏览器兼容问题,IE8以下版本的浏览器不支持
2.如果浏览器设置为隐私模式,那我们将无法读取到LocalStorage
3.LocalStorage受到同源策略的限制

(3)SessionStorage: 是HTML5提出来的存储方案,主要用于临时保存同一窗口(或标签页)的数据,刷新页面时不会删除,关闭窗口或标签页之后将会删除这些数据。

2. Cookie有哪些字段

Value:值
Size: 大小
Name:名称
Path:可以访问此cookie的页面路径
Secure: 指定是否使用HTTPS安全协议发送CookieDomain:可以访问该cookie的域名
HTTP: 该字段包含httpOnly属性 ,该属性用来设置cookie能否通过脚本来访问。
Expires/Max-size:超时时间

3. 前端储存的方式有哪些?

cookies
localStorage
sessionStorage
IndexedDB

4. IndexedDB有哪些特点?

  • 键值对储存:所有类型的数据都可以直接存入,包括 JavaScript 对象。数据以"键值对"的形式保存,每一个数据记录都有对应的主键,主键是独一无二的。
  • 异步:操作时不会锁死浏览器,用户依然可以进行其他操作,这与 LocalStorage 形成对比,后者的操作是同步的。
  • 储存空间大:IndexedDB 的储存空间比 LocalStorage 大得多,一般来说不少于 250MB,甚至没有上限。
  • 支持二进制储存:IndexedDB 不仅可以储存字符串,还可以储存二进制数据

5. localstorage会遇到什么安全问题

1.采用明文存储,所以在存储的时候最好在服务器端进行加密
2.可能会被植入广告追踪标志
3.用户定期清除浏览器缓存有助于 cookie 更有效地发挥作用,但是会丢失localstorage中存储的数据

6. token为什么要设置过期时间,时限多长合适?

安全性问题,如果不设置过期时间,那么token 容易被盗,就会产生不可估量的影响。比如支付宝因为涉及强资金安全性,如果时长设置过长可能导致用户本人离开后由他人操作故意导致恶心资金流转的问题等。

那么token 时限多长合适呢?这就需要根据业务性来区分,涉及支付类,对数据账号安全特别敏感的,建议每次都重新登录;对于日常使用的,比如学习类APP,工具类APP,不涉及到金额,token 时限可以长点。

五、浏览器同源策略

1. 什么是跨域?

当一个请求url的协议域名端口三者之间任意一个与当前页面url不同即为跨域。

2. 什么是同源策略

跨域问题其实就是浏览器的同源策略造成的。同源策略是一种安全机制,如果两个url协议、域名、端口任意一个不相同,则这两个url就是不同源的,他们的请求就算是跨域。

同源政策主要限制了三个方面:

  • 当前域下的 js 脚本不能够访问其他域下的 cookie、localStorage 和 indexDB。
  • 当前域下的 js 脚本不能够操作访问操作其他域下的 DOM。
  • 当前域下 ajax 无法发送跨域请求。

同源政策的目的主要是为了保证用户的信息安全,它只是对 js 脚本的一种限制,并不是对浏览器的限制,对于一般的 img、或者script 脚本请求都不会有跨域的限制,这是因为这些操作都不会通过响应结果来进行可能出现安全问题的操作。

3. 为什么会出现跨域?

这里假设有A网站,如果没有相关的安全策略,Web世界就是开放的。那么任何资源都可以接入A网站,,但这样会造成无序或者混沌的局面,出现很多不可控的问题。

比如A网站请求到了一个恶意网站(B)的恶意脚本,就可以修改A网站的DOM、CSSOM结构,还可以插入一段JavaScript脚本,甚至可以获取A网站的用户名和密码及Cookie信息。

因此,我们就需要一种安全策略来保障我们的隐私和数据的安全。

4. 如何解决跨越问题

(1)CORS

跨域资源共享(CORS) 是一种机制,它使用额外的 HTTP 头来告诉浏览器,让运行在一个 origin 上的Web应用被允许访问来自不同源服务器上的指定的资源。CORS 是在后端服务器做相应配置的。

// 配置关键代码Access-Control-Allow-Origin与Access-Control-Allow-Methods
app.use((req,res,next)=>{
    res.header('Access-Control-Allow-Origin', 'http://localhost:8080')
    res.header('Access-Control-Allow-Methods', 'GET, PUT, POST, OPTIONS')
    next()
})

Access-Control-Allow-Origin:设定请求源,就告诉了浏览器。如果请求我的资源的页面是我这个响应头里记录了的"源",则不要拦截此响应,允许数据通行。

(2)JSONP

其原理就是利用<script>标签没有跨域限制,通过标签的 src 属性,发送带有callback参数的GET请求,服务端将接口返回数据拼凑到callback函数中,返回给浏览器,浏览器解析执行,从而前端拿到callback函数返回的数据。

1)原生JS实现:

<script>
    let script = document.createElement('script');
    script.type = 'text/javascript';
    // 传参一个回调函数名给后端,方便后端返回时执行这个在前端定义的回调函数
    script.src = 'http://www.domain2.com:8080/login?user=admin&callback=handleCallback';
    document.head.appendChild(script);
    // 回调执行函数
    function handleCallback(res) {
        alert(JSON.stringify(res));
    }
 </script>

服务端返回如下(返回时即执行全局函数):

handleCallback({"success": true, "user": "admin"})

2)Vue axios实现:

this.$http = axios;
this.$http.jsonp('http://www.domain2.com:8080/login', {
    params: {},
    jsonp: 'handleCallback'
}).then((res) => {
    console.log(res); 
})

后端node.js代码:

let querystring = require('querystring');
let http = require('http');
let server = http.createServer();
server.on('request', function(req, res) {
    var params = querystring.parse(req.url.split('?')[1]);
    var fn = params.callback;
    // jsonp返回设置
    res.writeHead(200, { 'Content-Type': 'text/javascript' });
    res.write(fn + '(' + JSON.stringify(params) + ')');
    res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');

JSONP的缺点:

  • 具有局限性, 仅支持get方法
  • 不安全,可能会遭受XSS攻击

(3)postMessage

它可用于解决以下方面的问题:

  • 页面和其打开的新窗口的数据传递
  • 多窗口之间消息传递
  • 页面与嵌套的iframe消息传递
  • 上面三个场景的跨域数据传递

postMessage(data,origin)方法接受两个参数:

  • data: 部分浏览器只支持字符串,传参时最好用JSON.stringify()序列化
  • origin: 协议+主机+端口号,也可以设置为"*",表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为"/"。

1)a.html:(domain1.com/a.html)

<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>       
    let iframe = document.getElementById('iframe');
    iframe.onload = function() {
        let data = {
            name: 'aym'
        };
        // 向domain2传送跨域数据
        iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.domain2.com');
    };
    // 接受domain2返回数据
    window.addEventListener('message', function(e) {
        alert('data from domain2 ---> ' + e.data);
    }, false);
</script>

2)b.html:(domain2.com/b.html)

<script>
    // 接收domain1的数据
    window.addEventListener('message', function(e) {
        alert('data from domain1 ---> ' + e.data);
        let data = JSON.parse(e.data);
        if (data) {
            data.number = 16;
            // 处理后再发回domain1
            window.parent.postMessage(JSON.stringify(data), 'http://www.domain1.com');
        }
    }, false);
</script>

(4)nginx代理

nginx代理跨域,实质和CORS跨域原理一样,通过配置文件设置请求响应头Access-Control-Allow-Origin字段

1)nginx配置解决iconfont跨域,浏览器跨域访问 js、css、img等常规静态资源被同源策略许可,但iconfont字体文件(eot|otf|ttf|woff|svg)例外,此时可在 nginx 的静态资源服务器中加入以下配置。

location / {
  add_header Access-Control-Allow-Origin *;
}

2)nginx反向代理接口跨域:通过 Nginx 配置一个代理服务器域名与 domain1 相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域访问。

#proxy服务器
server {
    listen       81;
    server_name  www.domain1.com;
    location / {
        proxy_pass   http://www.domain2.com:8080;  #反向代理
        proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
        index  index.html index.htm;
        # 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
        add_header Access-Control-Allow-Origin http://www.domain1.com;  #当前端只跨域不带cookie时,可为*
        add_header Access-Control-Allow-Credentials true;
    }
}

(5)nodejs 中间件代理跨域

node中间件实现跨域代理,原理大致与nginx相同,都是通过启一个代理服务器,实现数据的转发。

1)非Vue框架的跨域

  • 前端代码:
let xhr = new XMLHttpRequest();
// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;
// 访问http-proxy-middleware代理服务器
xhr.open('get', 'http://www.domain1.com:3000/login?user=admin', true);
xhr.send();
  • 中间件服务器代码:
let express = require('express');
let proxy = require('http-proxy-middleware');
let app = express();
app.use('/', proxy({
    // 代理跨域目标接口
    target: 'http://www.domain2.com:8080',
    changeOrigin: true,
    // 修改响应头信息,实现跨域并允许带cookie
    onProxyRes: function(proxyRes, req, res) {
        res.header('Access-Control-Allow-Origin', 'http://www.domain1.com');
        res.header('Access-Control-Allow-Credentials', 'true');
    },
    // 修改响应信息中的cookie域名
    cookieDomainRewrite: 'www.domain1.com'  // 可以为false,表示不修改
}));
app.listen(3000);
console.log('Proxy server is listen at port 3000...');

2)Vue 框架的跨域

module.exports = {
    entry: {},
    module: {},
    ...
    devServer: {
        historyApiFallback: true,
        proxy: [{
            context: '/login',
            target: 'http://www.domain2.com:8080',  // 代理跨域目标接口
            changeOrigin: true,
            secure: false,  // 当代理某些https服务报错时用
            cookieDomainRewrite: 'www.domain1.com'  // 可以为false,表示不修改
        }],
        noInfo: true
    }
}

(6)document.domain + iframe跨域

此方案仅限主域相同,子域不同的跨域应用场景。实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。

1)父窗口:(domain.com/a.html)

<iframe id="iframe" src="http://child.domain.com/b.html"></iframe>
<script>
    document.domain = 'domain.com';
    let user = 'admin';
</script>

2)子窗口:(child.domain.com/a.html)

<script>
    document.domain = 'domain.com';
    // 获取父窗口中变量
    console.log('get js data from parent ---> ' + window.parent.user);
</script>

(7)WebSocket协议跨域

WebSocket protocol是HTML5一种新的协议,它实现了浏览器与服务器全双工通信,同时允许跨域通讯。 原生 WebSocket API使用起来不太方便,我们使用 Socket.io,它很好地封装了 webSocket 接口,提供了更简单、灵活的接口。

1)前端代码:

<div>user input:<input type="text"></div>
<script src="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.js"></script>
<script>
let socket = io('http://www.domain2.com:8080');
// 连接成功处理
socket.on('connect', function() {
    // 监听服务端消息
    socket.on('message', function(msg) {
        console.log('data from server: ---> ' + msg); 
    });
    // 监听服务端关闭
    socket.on('disconnect', function() { 
        console.log('Server socket has closed.'); 
    });
});
document.getElementsByTagName('input')[0].onblur = function() {
    socket.send(this.value);
};
</script>

2)Nodejs socket后台:

let http = require('http');
let socket = require('socket.io');
// 启http服务
let server = http.createServer(function(req, res) {
    res.writeHead(200, {
        'Content-type': 'text/html'
    });
    res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');
// 监听socket连接
socket.listen(server).on('connection', function(client) {
    // 接收信息
    client.on('message', function(msg) {
        client.send('hello:' + msg);
        console.log('data from client: ---> ' + msg);
    });
    // 断开处理
    client.on('disconnect', function() {
        console.log('Client socket has closed.'); 
    });
});

5. 正向代理和反向代理的区别

  • 正向代理:

客户端想获得一个服务器的数据,但是因为种种原因无法直接获取。于是客户端设置了一个代理服务器,并且指定目标服务器,之后代理服务器向目标服务器转交请求并将获得的内容发送给客户端。这样本质上起到了对真实服务器隐藏真实客户端的目的。

  • 反向代理:

服务器为了能够将工作负载分布到多个服务器来提高网站性能 (负载均衡),当其收到请求后,会首先根据转发规则来确定请求应该被转发到哪个服务器上,然后将请求转发到对应的真实服务器上。这样本质上起到了对客户端隐藏真实服务器的作用。 一般使用反向代理后,需要通过修改 DNS 让域名解析到代理服务器 IP,这时浏览器无法察觉到真正服务器的存在,当然也就不需要修改配置了。

两者区别如图示:

正向代理和反向代理的结构是一样的,都是 client-proxy-server 的结构,它们主要的区别就在于中间这个 proxy 是哪一方设置的。在正向代理中,proxy 是 client 设置的,用来隐藏 client;而在反向代理中,proxy 是 server 设置的,用来隐藏 server。

6. Nginx的概念及其工作原理

Nginx 是一款轻量级的 Web 服务器,也可以用于反向代理、负载平衡和 HTTP 缓存等。Nginx 使用异步事件驱动的方法来处理请求,是一款面向性能设计的 HTTP 服务器。

传统的 Web 服务器如 Apache 是 process-based 模型的,而 Nginx 是基于event-driven模型的。正是这个主要的区别带给了 Nginx 在性能上的优势。

Nginx 架构的最顶层是一个 master process,这个 master process 用于产生其他的 worker process,这一点和Apache 非常像,但是 Nginx 的 worker process 可以同时处理大量的HTTP请求,而每个 Apache process 只能处理一个。

7. Vue.config.js配置proxy为什么能解决跨域

首先浏览器是禁止跨域的,但是服务端不禁止,在本地运行 npm run dev 等命令时实际上是用 node 运行了一个服务器,因此 Vue 的转发机制 proxyTable 实际上是将请求发给自己的服务器,再由服务器转发给后台服务器,做了一层代理。Vue的proxyTable 用的是 http-proxy-middleware 中间件, 因此不会出现跨域问题。

8. 浏览器对哪些跨域是允许的

<script><img><iframe><link><a>等标签都可以加载跨域资源

六、前端进阶知识

1. CDN

CDN(Content Delivery Network,内容分发网络)是指一种通过互联网互相连接的电脑网络系统,利用最靠近每位用户的服务器,更快、更可靠地将音乐、图片、视频、应用程序及其他文件发送给用户。CDN一般会用来托管Web资源(包括文本、图片、脚本、软件、文档、应用程序),使用CDN可以加速这些资源的访问。使用场景如下:

(1)直播传送:直播本质上是使用流媒体进行传送,CDN也是支持流媒体传送的,所以直播完全可以使用CDN来提高访问速度。

(2)使用第三方的CDN服务:如果想要开源一些项目,可以使用第三方的CDN服务

(3)使用CDN进行静态资源的缓存:将自己网站的静态资源放在CDN上,比如js、css、图片等;可以将整个项目放在CDN上,完成一键部署。

2. 懒加载与预加载

这两种方式都是提高网页性能的方式,两者主要区别是一个是提前加载,一个是迟缓甚至不加载。懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力。

  • 懒加载也叫延迟加载,指的是在长网页中延迟加载图片的时机,当用户需要访问时,再去加载,这样可以提高网站的首屏加载速度,提升用户的体验,并且可以减少服务器的压力,它适用于图片很多的电商网站。
  • 预加载指的是将所需的资源提前请求加载到本地,这样后面在需要用到时就直接从缓存取资源。

3. 什么是回流(重排)?

当渲染树中部分或者全部元素的尺寸、结构、位置、内容等属性发生变化时,浏览器会重新渲染部分或者全部文档的过程就称为回流

下面这些操作会导致回流:

  • 页面的首次渲染
  • 浏览器的窗口大小发生变化
  • 元素的内容发生变化
  • 元素的尺寸或者位置发生变化
  • 元素的字体大小发生变化
  • 添加或者删除可见的DOM元素

4. 什么是重绘?

当页面中某些元素的样式发生变化,但是不会影响其在文档流中的位置时,浏览器就会对元素进行重新绘制,这个过程就是重绘

下面这些操作会导致重绘:

  • color、background-color、background-image
  • border-radius、visibility、box-shadow

注意:当触发回流时,一定会触发重绘,但是重绘不一定会引发回流

5. 如何减少回流与重绘?

  • 不要使用 table 布局, 一个小的改动可能会使整个 table 进行重新布局
  • 不要频繁操作元素的样式
  • 使用 absolute/fixed,使元素脱离文档流,这样他们发生变化就不会影响其他元素
  • 避免频繁操作DOM,可以创建一个文档片段 documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中
  • 将元素先设置 display: none,操作结束后再把它显示出来,因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘。
  • 将DOM的多个读操作(或者写操作)放在一起,而不是读写操作穿插着写

6. 浏览器的渲染队列

浏览器针对页面的回流与重绘,进行了自身的优化——渲染队列。浏览器会将所有的回流、重绘的操作放在一个队列中,当队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会对队列进行批处理。这样就会让多次的回流、重绘变成一次回流重绘。

7. documentFragment (文档碎片)是什么?

当我们把一个 DocumentFragment 节点插入文档树时,插入的不是 DocumentFragment 自身,而是它的所有子孙节点。在频繁的DOM操作时,我们就可以将DOM元素插入DocumentFragment,之后一次性的将所有的子孙节点插入文档中。DocumentFragment不是真实 DOM 树的一部分,它的变化不会触发 DOM 树的重新渲染,这样就大大提高了页面的性能。

运用场景:假如有 10000 个元素需要添加到页面上,你觉得怎么操作性能最好?

const oFrag = document.createDocumentFragment()    
for (let i = 1; i <= 1000; i++) {     
    const oDiv = document.createElement('div')      
    oDiv.innerHTML = i     
    oFrag.appendChild(oDiv)   
}    
document.body.appendChild(oFrag)

8. 如何优化动画?

一般情况下,动画需要频繁的操作DOM,这就会导致页面的性能问题,我们可以将动画的 position 属性设置为 absolute 或者 fixed,将动画脱离文档流,这样他的回流就不会影响到页面了。

9. 如何对项目中的图片进行优化?

  1. 不用图片。很多时候会使用到很多修饰类图片,其实这类修饰图片完全可以用 CSS 去代替。
  2. 对于移动端来说,屏幕宽度就那么点,完全没有必要去加载原图浪费带宽。一般图片都用 CDN 加载,可以计算出适配屏幕的宽度,然后去请求相应裁剪好的图片。
  3. 小图使用 base64 格式
  4. 将多个图标文件整合到一张图片中(雪碧图)
  5. 选择正确的图片格式:
    • 对于能够显示 WebP 格式的浏览器尽量使用 WebP 格式。因为 WebP 格式具有更好的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量,缺点就是兼容性并不好
    • 小图使用 PNG,其实对于大部分图标这类图片,完全可以使用 SVG 代替
    • 照片使用 JPEG

10. 什么是虚拟列表

虚拟列表其实是按需显示的一种实现,即只对可见区域进行渲染,对非可见区域中的数据不渲染或部分渲染的技术,从而达到极高的渲染性能。

假设有1万条记录需要同时渲染,我们屏幕的可见区域的高度为500px,而列表项的高度为50px,则此时我们在屏幕中最多只能看到10个列表项,那么在首次渲染的时候,我们只需加载10条即可。

11. 如何实现虚拟列表

虚拟列表的实现,实际上就是在首屏加载的时候,只加载可视区域内需要的列表项,当滚动发生时,动态通过计算获得可视区域内的列表项,并将非可视区域内存在的列表项删除。

<div class="infinite-list-container">
    <div class="infinite-list-phantom"></div>
    <div class="infinite-list">
      <!-- item-1 -->
      <!-- item-2 -->
      <!-- ...... -->
      <!-- item-n -->
    </div>
</div>
  • infinite-list-container 为可视区域的容器
  • infinite-list-phantom 为容器内的占位,高度为总列表高度,用于形成滚动条
  • infinite-list 为列表项的渲染区域

接着,监听infinite-list-containerscroll事件,获取滚动位置scrollTop

  • 假定可视区域高度固定,称之为screenHeight
  • 假定列表每项高度固定,称之为itemSize
  • 假定列表数据称之为listData
  • 假定当前滚动位置称之为scrollTop

则可推算出:

  • 列表总高度listHeight = listData.length * itemSize
  • 可显示的列表项数visibleCount = Math.ceil(screenHeight / itemSize)
  • 数据的起始索引startIndex = Math.floor(scrollTop / itemSize)
  • 数据的结束索引endIndex = startIndex + visibleCount
  • 列表显示数据为visibleData = listData.slice(startIndex,endIndex)

当滚动后,由于渲染区域相对于可视区域已经发生了偏移,此时我需要获取一个偏移量startOffset,通过样式控制将渲染区域偏移至可视区域中。

  • startOffset = scrollTop - (scrollTop % itemSize)

当滚动发生后,scroll事件会频繁触发,很多时候会造成重复计算的问题,从性能上来说无疑存在浪费的情况。可以使用 IntersectionObserver 替换监听scroll事件。

12. 什么是单点登录?

单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一。

SSO 的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。SSO 一般都需要一个独立的认证中心(passport),子系统的登录均得通过 passport,子系统本身将不参与登录操作。

当一个系统成功登录以后,passport 将会颁发一个令牌给各个子系统,子系统可以拿着令牌会获取各自的受保护资源,为了减少频繁认证,各个子系统在被 passport 授权以后,会建立一个局部会话,在一定时间内可以无需再次向 passport 发起认证。

13. 浏览器与Node事件循环

参考文章:juejin.cn/post/719880…

14. 事件冒泡、事件捕获、事件委托

参考文章:juejin.cn/post/719258…

15. Babel的原理是什么?

Babel 的主要工作是对代码进行转译 (解决兼容, 解析执行一部分代码)

let a = 1 + 1    =>  var a = 2

转译分为三阶段:

  • 解析(Parse),将代码解析⽣成抽象语法树 AST,也就是词法分析与语法分析的过程
  • 转换(Transform),对语法树进⾏变换方面的⼀系列操作。通过 babel-traverse,进⾏遍历并作添加、更新、删除等操作
  • ⽣成(Generate),通过 babel-generator 将变换后的 AST 转换为 JS 代码

16. 说说前端渲染和后端渲染,以及他们的优缺点

前端渲染:指的是后端返回JSON数据,前端利用预先写的html模板,循环读取JSON数据,拼接字符串并插入页面

后端渲染:前端请求,后端用后台模板引擎直接生成html,前端接收到数据之后,直接插入页面

区别

前端渲染后端渲染
页面呈现速度主要受限于带宽和客户端机器的好坏,优化的好,可以逐步动态展开内容,感觉上会更快一点快,受限于用户的带宽
流量消耗多一点点(一个前端框架大概50KB)少一点点(可以省去前端框架部分的代码)
可维护性好,前后端分离,各施其职,代码一目明了差(前后端东西放一起,不利于维护)
SEO友好度差,大量使用Ajax,多数浏览器不能抓取Ajax数据
编码效率高,前后端各自只做自己擅长的东西,后端最后只输出接口,不用管页面呈现,只要前后端人员能力不错,效率不会低低(这个跟不同的团队不同,可能不对)

17. 冒泡排序及其优化

升序冒泡:两次循环,相邻元素两两比较,如果前面的大于后面的就交换位置

降序冒泡:两次循环,相邻元素两两比较,如果前面的小于后面的就交换位置

// 升序冒泡
function maopao(arr){
  const array = [...arr]
  for(let i = array.length; i > 0; i--){
    for(let j = 0; j < i - 1; j++) {
      if (array[j] > array[j + 1]) {
        let temp = array[j]
        array[j] = array[j + 1]
        array[j + 1] = temp
      }
    }
  }
  return array
}

看起来没问题,不过一般生产环境都不用这个,原因是效率低下,就算你给一个已经排好序的数组,它也会走一遍流程,白白浪费资源。所以有没有什么好的解决方法呢?答案:加个标识,如果已经排好序了就直接跳出循环。

// 优化版:
function maopao1(arr){
  const array = [...arr]
  for(let i = array.length; i > 0; i--){
    let isOk = true
    for(let j = 0; j < i - 1; j++) {
      if (array[j] > array[j + 1]) {
        let temp = array[j]
        array[j] = array[j + 1]
        array[j + 1] = temp
        isOk = false
      }
    }
    if (isOk) {
      break
    }
  }
  return array
}

七、Node.js

1. Node是什么?

Node.js是一个基于 Chrome V8 引擎的 JavaScript 运行环境,这个环境就好比是服务器上的浏览器,但正是因为有了它才使得 js 变成了一门后台语言。

2. Node解决了哪些问题?

3. Node 的应用场景

4. 什么是模块化? 模块化有什么好处?

  • 1.什么叫模块化?(模块 == js文件)

    • 一个js文件可以引入另一个js文件中的数据, 这种开发方式就叫做模块化开发
  • 2.模块化开发好处?

    • (1)将功能分离出来

    • (2)按需导入

    • (3)避免变量污染

5. 模块缓存机制

  • 1.当一个模块第一次会加载时,nodejs 会执行里面的 js 代码,并且导出模块
  • 2.nodejs 会将导出的模块放入缓存中
  • 3.当重复导入一个模块的时候,nodejs 会先从缓存中读取模块。 如果缓存中有,就从缓存读取;缓存没有重复步骤1

6. require和import的区别

  1. require 是 CommonJS 的标准,import 是 ES6 的标准
  2. require 是赋值过程并且是运行时才执行,也就是同步加载
  3. require 可以理解为一个全局方法,因为它是一个方法所以意味着可以在任何地方执行。
  4. import 是解构过程并且是编译时执行,理解为异步加载
  5. import 会提升到整个模块的头部,具有置顶性,但是建议写在文件的顶部。

7. 模块化的发展历程

image.png