Skip to content
本页目录

运行环境

运行环境主要指项目上线后的环境,主要设计的面试考点有:

  • 页面加载过程
  • 性能和体验优化
  • 安全

1. 页面加载

题目

  • 从输入url到得到html的详细过程
  • window.onload 和 DOMContentLoaded 的区别

知识点

浏览器加载资源的过程

加载资源的形式

  • 输入 url 加载 html
  • http://baidu.com
  • 加载 html 中的静态资源
  • <script src="/static/js/jquery.js"></script>

加载一个资源的过程

  • 浏览器根据 DNS 服务器得到域名的 IP 地址
  • 向这个 IP 的机器发送 http 请求
  • 服务器收到、处理并返回 http 请求
  • 浏览器得到返回内容

浏览器渲染页面的过程

  • 根据 HTML 结构生成 DOM Tree
  • 根据 CSS 生成 CSS Rule
  • 将 DOM 和 CSSOM 整合形成 RenderTree
  • 根据 RenderTree 开始渲染和展示
  • 遇到<script>时,会执行并阻塞渲染

几个示例

最简单的页面

html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <p>test</p>
</body>
</html>

引用 css

css 内容

css
div {
    width: 100%;
    height: 100px;
    font-size: 50px;
}

html 内容

html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <link rel="stylesheet" type="text/css" href="test.css">
</head>
<body>
    <div>test</div>
</body>
</html>

最后思考为何要把 css 放在 head 中???

引入 js

js 内容

js
document.getElementById('container').innerHTML = 'update by js'

html 内容

html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <div id="container">default</div>
    <script src="index.js"></script>
    <p>test</p>
</body>
</html>

思考为何要把 JS 放在 body 最后???

有图片

html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <p>test</p>
    <p><img src="test.png"/></p>
    <p>test</p>
</body>
</html>

引出window.onloadDOMContentLoaded

js
window.addEventListener('load', function () {
    // 页面的全部资源加载完才会执行,包括图片、视频等
})
document.addEventListener('DOMContentLoaded', function () {
    // DOM 渲染完即可执行,此时图片、视频还可能没有加载完
})

题目解答

从输入url到得到html的详细过程

  • 浏览器根据 DNS 服务器得到域名的 IP 地址
  • 向这个 IP 的机器发送 http 请求
  • 服务器收到、处理并返回 http 请求
  • 浏览器得到返回内容

window.onload 和 DOMContentLoaded 的区别

  • 页面的全部资源加载完才会执行,包括图片、视频等
  • DOM 渲染完即可执行,此时图片、视频还没有加载完

2. 性能优化

这本身就是一个大问题,会直接被问“如何提高前端性能”。而且这些性能优化的事儿,已经远远超出了 JS 知识的范畴,只不过面试会问到,我必须告诉大家一些基本的回答方式。

这种问题没有正确答案,特别是不同层面关注的点不一样

优化原则和方向

原则

  • 多使用内存、缓存或者其他方法
  • 减少 CPU 计算、较少网络

方向

  • 加载页面和静态资源
  • 页面渲染

加载资源优化

  • 静态资源的压缩合并(JS代码压缩合并、CSS代码压缩合并、雪碧图)
  • 静态资源缓存(资源名称加 MD5 戳)
  • 使用 CND 让资源加载更快
  • 使用 SSR 后端渲染,数据直接突出到 HTML 中

渲染优化

  • CSS 放前面 JS 放后面
  • 懒加载(图片懒加载、下拉加载更多)
  • 减少DOM 查询,对 DOM 查询做缓存
  • 减少DOM 操作,多个操作尽量合并在一起执行(DocumentFragment
  • 节流和防抖
  • 尽早执行操作(DOMContentLoaded

详细解说

静态资源的压缩合并

如果不合并,每个都会走一遍之前介绍的请求过程

html
<script src="a.js"></script>
<script src="b.js"></script>
<script src="c.js"></script>

如果压缩了,就只走一遍请求过程

html
<script src="abc.js"></script>

静态资源缓存

通过连接名称控制缓存

html
<script src="abc_1.js"></script>

只有内容改变的时候,链接名称才会改变

html
<script src="abc_2.js"></script>

使用 CND 让资源加载更快

html
<script src="https://cdn.bootcss.com/zepto/1.0rc1/zepto.min.js"></script>

使用 SSR 后端渲染

如果提到 Vue 和 React 时,可以说一下

CSS 放前面 JS 放后面

将浏览器渲染的时候,已经提高

懒加载

一开始先给为 src 赋值成一个通用的预览图,下拉时候再动态赋值成正式的图片

html
<img src="preview.png" data-realsrc="abc.png"/>

DOM 查询做缓存

两端代码做一下对比

js
// 不缓存 DOM 查询结果
for (let = 0; i < document.getElementsByTagName('p').length; i++) {
    // 每次循环,都会计算 length ,频繁进行 DOM 查询
}

// 缓存 DOM 查询结果
const pList = document.getElementsByTagName('p')
const length = pList.length
for (let i = 0; i < length; i++) {
    // 缓存 length ,只进行一次 DOM 查询
}

总结:DOM 操作,无论查询还是修改,都是非常耗费性能的,尽量减少

合并 DOM 插入

DOM 操作是非常耗费性能的,因此插入多个标签时,先插入 Fragment 然后再统一插入DOM

js
const listNode = document.getElementById('list')

// 创建一个文档片段,此时还没有插入到 DOM 树中
const frag = document.createDocumentFragment()

// 执行插入
for(let x = 0; x < 10; x++) {
    const li = document.createElement("li")
    li.innerHTML = "List item " + x
    frag.appendChild(li)
}

// 都完成之后,再插入到 DOM 树中
listNode.appendChild(frag)

尽早执行操作

js
window.addEventListener('load', function () {
    // 页面的全部资源加载完才会执行,包括图片、视频等
})
document.addEventListener('DOMContentLoaded', function () {
    // DOM 渲染完即可执行,此时图片、视频还可能没有加载完
})

节流和防抖

例如要在文字改变时触发一个 change 事件,通过 keyup 来监听。使用防抖

js
const textarea = document.getElementById('text')
let timer
textarea.addEventListener('keyup', () => {
    if (timer) {
        clearTimeout(timer)
    }
    timer = setTimeout(() => {
        // 触发 change 事件

        // 清空定时器
        timer = null
    }, 100)
})

可以把防抖时间单独抽离出来实现

js
// 手写防抖
function debounce(fn, delay = 200) {
    // timer 在闭包中
    let timer = null

    // 返回一个函数
    return function() {
        if (timer) {
            clearTimeout(timer)
        }
        timer = setTimeout(() => {
            fn.apply(this, arguments) // 透传 this 和函数参数
            timer = null // 触发过了,重新计时
        }, delay)
    }
}

const textarea = document.getElementById('text')
textarea.addEventListener('keyup', debounce(() => {
    // 触发 change 事件
}))

在拖拽时,随时要检测当前的位置信息(如是否覆盖了目标元素的位置),可用节流

js
// <p id="p1" draggable="true">拖动我!</p>

const p1 = document.getElmentById('p1')
p1.addEventListener('drag', e => {
    // 这样会打印很频繁,如果在其中再做一些 DOM 查询,那就出现卡顿
    console.log('鼠标位置', e.offsetX, e.offsetY)
})

使用节流

js
// 手写节流
function throttle(fn, delay = 100) {
    // timger 在闭包中
    let timer = null

    // 返回一个函数
    return function(){
        //当我们发现这个定时器存在时,则表示定时器已经在运行中,还没到该触发的时候,则 return
        if (timer) {
            return
        }
        // 定时器不存在了,说明已经触发过了,重新计时
        timer = setTimeout(()=>{
            fn.apply(this, arguments) // 透传 this 和函数参数
            timer = null // 清空定时器
        }, delay)
    }
}

// 再次实现
p1.addEventListener('drag', throttle(e => {
    console.log('鼠标位置', e.offsetX, e.offsetY)
})

3. 安全性

题目

【面试】常见的 web 攻击方式有哪些,简述原理?如何预防?

解决

关于前端安全的知识,建议阅读《白帽子讲web安全》,作者也是一位很传奇的人物,这本书写的浅显易懂,很适合前端工程师阅读。

常见的 web 攻击方式有哪些,简述原理?如何预防?

或与大家听过有一个“SQL注入”的攻击方式。

例如做一个系统的登录界面,输入用户名和密码,提交之后,后端直接拿到数据就拼接 SQL 语句去查询数据库。如果在输入时进行了恶意的 SQL 拼装,那么最后生成的 SQL 就会有问题。但是现在稍微大型的一点系统,都不会这么做,从提交登录信息到最后拿到授权,都经过层层的验证。因此,SQL 注入都只出现在比较低端小型的系统上。

前端端最常见的攻击就是 XSS(Cross Site Scripting,跨站脚本攻击)

很多大型网站(例如 FaceBook 都被 XSS 攻击过)。举一个例子,我在一个博客网站正常发表一篇文章,输入汉字、英文和图片,完全没有问题。

但是如果我写的是恶意的 js 脚本,例如获取到document.cookie然后传输到自己的服务器上,那我这篇博客的每一次浏览,都会执行这个脚本,都会把自己的 cookie 中的信息偷偷传递到我的服务器上来。

预防 XSS 攻击就得对输入的内容进行过滤,过滤掉一切可以执行的脚本和脚本链接。大家可以参考xss.js这个开源工具。

简单总结一下,XSS 其实就是攻击者事先在一个页面埋下攻击代码,让登录用户去访问这个页面,然后偷偷执行代码,拿到当前用户的信息。

还有一个比较常见的攻击就是 CSRF/XSRF(Cross-site request forgery,跨站请求伪造)

它是借用了当前操作者的权限来偷偷的完成某个操作,而不是拿到用户的信息。

例如,一个购物网站,购物付费的接口是http://buy.com/pay?id=100,而这个接口在使用时没有任何密码或者 token 的验证,只要打开访问就付费购买了。

一个用户已经登录了http://buy.com在选择商品时,突然收到一封邮件,而这封邮件正文有这么一行代码<img src="http://buy.com/pay?id=100"/>,他访问了邮件之后,其实就已经完成了购买。

预防 CSRF 就是加入各个层级的权限验证,例如现在的购物网站,只要涉及到现金交易,肯定输入密码或者指纹才行。 最简单的解决方案就是把提交类的接口设计为POST请求。