运行环境介绍
- 运行环境即浏览器(server端有nodejs,移动端在app/微信上)
- 下载网页代码,渲染出页面,期间会执行若干JS
- 要保证代码在浏览器中:稳定且高效
- 网页加载过程
- 性能优化
- 安全
页面加载和渲染过程
- 资源的形式
- html代码
- 媒体文件,如图片、视频等
- javascript CSS
- 加载过程:
- DNS解析(Domain Name Server):域名->IP地址(不直接使用IP是因为不同区域IP地址不同,域名更好记)
- 建立TCP连接,浏览器根据IP地址向服务器发起http请求
- 服务器处理http请求,并返回html给浏览器
- 渲染过程:
- 根据HTML代码生成DOM Tree(Document Object Model)
- 根据CSS代码生成CSSOM(CSS Object Model,CSS对象模型)
- 将DOM Tree和CSSOM整合形成Render Tree(渲染树)
- 根据Render Tree渲染页面
- 遇到
<script>
则暂停渲染(因为JS有可能修改DOM结构),优先加载并执行JS代码,完成再继续
- 直至把Render Tree渲染完成
- 例子:渲染到img时如果太大还未加载完成也不会阻塞DOM树的渲染,直接往下渲染,等图片加载完成再插入
window.onload和DOMContentLoaded
- window.onload:页面的全部资源加载完才会执行,包括图片、视频等
- DOMContentLoaded:DOM渲染完即可执行,此时图片、视频还可能没有加载完
- 注意:使用DOMContentLoaded来触发JS比window.onload更好,不需要等图片、视频都加载完成才触发JS的加载,这样页面渲染会更快。
性能优化
- 原则:
- 多使用内存、缓存或其他方法
- 减少CPU计算量,减少网络加载耗时
- (适用于所有编程的性能优化一空间换时间)
- 从何入手:
- 让加载更快
- 让渲染更快
- 减少回流、重绘:
- **合并DOM操作**:比如我们要向页面添加多个img元素,如果单独加进去就会频繁的回流+重绘,此时我们就可以使用
document.createDocumentFragment()
将多个img合并到Fragment中再统一加入。
- 避免频繁读取会引发回流/重绘的属性(见上),如果确实需要多次使用,就用一个变量缓存起来。
- 将动画效果应用到position属性为absolute或fixed的元素上,对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。
让加载更快
- 减少资源体积:压缩代码
- 比如使用webpack在production环境下进行打包时,就会自动压缩代码至1/3的大小,在浏览器上进行反解析再进行渲染。
- 减少访问次数:
- 合并代码:比如webpack中,在index.js中引入a.js、b.js,打包后只生成一个bundle.js,这就是合并代码。(加载3次3kb的文件不如加载1次9kb快,这和网络请求有关系)
- SSR服务器端渲染:不需要通过ajax发送请求。
- 缓存:原本需要发送多个请求的数据直接在缓存中获取即可减少访问次数。
- 使用更快的网络:CDN
- CDN是分区域的,也就是使用CDN的时候上海和北京对同一个网站的IP地址是不同的。CDN会选择一个离用户最近的CDN边缘节点来响应用户的请求,这样海南移动用户的请求就不会千里迢迢跑到北京电信机房的服务器(假设源站部署在北京电信机房)上了
- 图片、js等静态资源采用CDN是很快的,我们经常使用的bootstrap就是使用的CDN
缓存
- 静态资源加hash后缀,根据文件内容计算hash
- 文件内容不变,则hash不变,则url不变
- url和文件不变,则会自动触发http缓存机制,返回304(提示用户资源未更新,到缓存中获取)【大部分web服务器都默认开启协商缓存】
- 例子:在使用webpack的例子webpack-demo中,配置webpack的输出文件名时采用的内容哈希值
[contenthash]
就会根据文件内容来生成哈希值(每一次生成的文件不会覆盖之前的文件,且只要内容变化名字就会发生改变)
- 后续引用webpack生成过哈希命名的文件的方法可参考“引用带hash的文件”
- 可参考强缓存与协商缓存
CDN
- CDN,Content Delivery Network,即内容分发网络。是根据地域做静态服务的,我们经常使用的bootstrap就是使用的CDN,他会加快加载速度。(CDN缓存)
- CDN不变且文件不变时,也会自动触发http缓存机制,返回304。
- 而百度这里用的bdstatic就不是CDN:
SSR
- SSR(服务器端渲染):server side render,将网页和数据一起加载,一起渲染
- 非SSR:先加载网页,再加载数据,再渲染数据
- 早先的JSP ASP PHP 都是SSR,现在的 vue 和 react 默认下不是ssr,但可以在配置下成为ssr
- 如果右键查看源码看不到input等元素标签,则为非ssr。
- SSR与非SSR
让渲染更快
- CSS放在head中,JS放在body最下面
- 尽早开始执行JS,用DOMContentLoaded触发
- 懒加载(图片懒加载,上滑加载更多)
- 对DOM查询进行缓存(DOM操作很耗性能)
- 频繁DOM操作,合并到一起插入DOM结构
- 让渲染更流畅:节流throttle防抖debounce
懒加载
- 懒加载:常见图片懒加载,也就是我们希望先加载第一张图片,后面的图片没显示在屏幕上的就先不加载,等到滑动到图片位置时再进行该图片的加载。
- 例子:一开始加载src地址下的图片,等到滑动到下一图片位置时将data-realsrc的属性值赋给src,此时即为加载图片abc(本例子中不进行图片位置的判断,实际根据DOM元素距离顶部的值来计算)
缓存DOM的查询
js的操作和DOM的操作完全不是一个数量级的,尽量避免多次操作DOM:
多个DOM操作合并
多个DOM操作一起插入到DOM结构时注意可通过createDocumentFragment()先创建一个文档片段,这个文档片段是保存在JS中的(不是在DOM树中):
尽早开始执行JS
使用DOMContentLoaded来触发JS比window.onload更好,不需要等图片、视频都加载完成才触发JS的加载,这样页面渲染会更快。
DOMContentLoaded是当DOM树渲染完即可执行,此时图片、视频还可能没有加载完(注意是DOM渲染完成而不是页面渲染完成)
防抖 debounce
- 防抖和节流的区别:防抖是在最后一次触发结束后n秒都没触发了才会执行事件函数,在此之前的调用都会被忽略;节流则是以第一次触发为准,在指定时间内最多执行一次,多余的调用都会被忽略
- 防抖是一段时间内重复触发则清空重算,永远只管最后一次触发;节流是一段时间内只管第一次触发
- 例子见下“节流”的事例
- 防抖:指定时间内连续多次触发事件时以最后一次触发为标准重新计算函数执行时间,直到最后一次触发n秒内没有再次触发才执行
- 比如:
- 监听一个输入框的文字变化后触发change事件,直接用keyup事件,则会频发触发change事件。如果使用防抖,则用户输入结束或暂停时,才会触发change事件。(防抖不仅限于change事件)
- 对某个按钮进行高频操作时,防止连续点击
- 使用场景:
- search搜索联想,用户在不断输入值时,用防抖来节约请求资源
- window触发resize的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次
- 防抖实际上是对定时器setTimeout的使用,但我们通常将其封装为debounce函数来使用
- 例子:
- 原本设定是输入框中每输入一个字符就触发一个打印,将输入框中的值打印出来
1 2 3 4
| <body> <input type="text" id="input1"> <script src="./debounce.js"></script> </body>
|
1 2 3 4
| const input1 = document.getElementById('input1') input1.addEventListener('keyup', function() { console.log(input1.value) })
|
- 采用防抖后,每次输入结束的半秒后才会触发事件进行打印(半秒是自己设置的):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| const input1 = documentgetElementById('input1')
input1.addEventListener('keyup', function () {
if (timer) { clearTimeout(timer) } timer = setTimeout(() => { console.log(input1.value)
timer = null },500) })
|
逻辑梳理:
- timer用于存储定时器id,输入a时,通过setTimeout给timer赋值,准备500毫秒后触发打印
- 紧接着输入s时,由于此时timer不为空,所以执行clearTimerout将之前的定时器清除,重新创建setTimeout给timer赋值,准备500毫秒后触发打印
- 依旧紧接着输入d时,一直到s都重复上一步逻辑
- 直到输入最后一个f时,清除了上一个timer后创建了新的timer,且500毫秒后未被清除,故打印asdfasdfasf,并将timer清空
封装debounce函数
封装为debounce函数,避免每次使用都要创建setTimeout:
1 2 3 4
| <body> <input type="text" id="input1" /> <script src="./debounce.js"></script> </body>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| function debounce(fn, delay = 500) { let timer = null return function () { if (timer) { clearTimeout(timer) } timer = setTimeout(() => { fn.apply(this, arguments) timer = null }, delay) } }
input1.addEventListener('keyup', debounce(function() { console.log(input1.value) }), 600)
|
其中fn.apply(this,arguments)
直接使用fn()也可以,使用**apply
是为了防止fn有this或者参数(arguments对象**)需要传入debounce函数(如果要传入this,则需注意传入的fn,即例子中的箭头函数不能是箭头函数) 【可参考下面“节流”中的apply解释】
Lodash内置的防抖函数
- 系统中一般自己封装是为了添加适配自己的参数,可以自定义
- 可以直接使用 Lodash 的防抖函数,不用自己上手写,该函数会返回一个新函数,会在最后一次调用时延迟指定时间再执行,而在此之前的调用都会被忽略。
- 提供一个 cancel 方法取消延迟的函数调用,以及 flush 方法立即调用
- 参数1:执行函数func
- 参数2:需要延迟的毫秒数wait
- 参数3:配置对象 决定如何调用 func 方法
- leading:指定在延迟开始前调用(默认false)也就是延迟触发前就先调用一次
- maxWait:设置 func 允许被延迟的最大值,也就是多次触发在maxWait时间内一定会执行一次,就算maxWait内没有停止连续触发也还是会触发func
- trailing:指定在延迟结束后调用(默认true)
- 掘金有使用例子
1 2 3 4 5 6 7 8 9 10 11
| import { debounce } from 'lodash-es'
export default { methods: { click: debounce(function () { }, 500) } }
|
- vue文档中有提到,但要注意:对于被重用的组件,需要特殊处理(改为在 created 生命周期钩子中创建这个预置防抖的函数),避免共享该函数后互相影响:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
import { debounce } from 'lodash-es'
export default { methods: { click: debounce(function () { }, 500) } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| export default { created() { this.debouncedClick = _.debounce(this.click, 500) }, unmounted() { this.debouncedClick.cancel() }, methods: { click() { } } }
|
节流 throttle
- 下面的代码效果可在codepen查看
- 节流:一段时间内连续触发事件以第一次触发为准, n 秒中只执行一次函数
- 比如:
- 拖拽一个元素时,要随时拿到该元素被拖拽的位置。这里直接用drag事件则会频发触发,很容易导致卡顿。而使用节流,则无论拖拽速度多快,都会每隔100ms触发一次(时间自定义)
- 注意:如果用防抖的话,就会出现卡顿的感觉,因为只在停止的时候执行了一次,这个时候就应该用节流,在一定时间内多次执行,会流畅很多(具体原因可见上方“防抖”中“防抖与节流的区别”)
- 监听滚动事件,比如是否滑到底部自动加载更多,用节流比防抖更合适
- 防抖会在事件触发后等待一段时间,然后执行事件处理函数。在滚动加载更多数据的情况下,使用防抖可能会导致用户滚动结束后一段时间才触发加载,这可能会让用户感到加载响应有延迟
- 例子:
- 设置一个可拖拽元素并直接用drag事件,拖拽时实时打印元素位置:
1 2 3 4 5
| <body> <div id="div1" draggable="true">可拖拽</div> <script src="./throttle.js"></script> </body>
|
1 2 3 4 5
| const div1 = document.getElementById('div1')
div1.addEventListener('drag', function(e) { console.log(e.offsetX, e.offsetY) })
|
- 采取节流后:
1 2 3 4 5 6 7 8 9 10 11 12
| const div1 = document.getElementById('div1')
let timer = null div1.addEventListener('drag', function (e) { if (timer) { return } timer = setTimeout(() => { console.log(e.offsetX, e.offsetY) timer = null },100) })
|
逻辑梳理:
- 和节流相似,都设置一个定时器,初始为空
- 元素开始拖拽时触发事件,根据setTimeout返回的id赋给timer,此时需要等待100毫秒才打印位置
- 由于快速拖动元素,100毫秒内再次出发drag事件,但此时timer有值,直接退出drag事件,直到100毫秒位置打印后timer清空才会再走第二步。以此实现无论拖拽速度多快,都会每隔100ms触发一次drag事件
- 封装为throttle函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| function throttle(fn, delay = 100) { let timer = null
return function () { if (timer) { return } timer = setTimeout(() => {
fn.apply(this, arguments) timer = null }, delay) } }
div1.addEventListener('drag', throttle(function(e) {
console.log('----this', this) console.log(e.offsetX, e.offsetY) }, 100))
|
使用apply的原因
- 总结: 使用 apply 将 throttle() 内部的事件对象e传到 fn() 内部,以便在回调函数fn()中处理更多事情
- 需要了解的两个要点:
- 注意:父级作用域是在 函数定义时 规定的,不需要管执行顺序。所以这里很特殊的是,fn() 是在使用 throttle() 时才定义的,所以fn() 内部的函数体的 父级作用域 是 全局作用域 而不是 throttle()
- drag事件的e是传给 throttle() 的,如果不特殊处理,fn() 内部并不能获取到e,毕竟父级作用域不是 throttle()
- apply的作用:参数1绑定this指向,参数2绑定调用该函数时传入的参数(即:将参数传入fn)(arguments对象)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| function throttle(fn, delay = 100) { let timer = null
return function () { if (timer) { return } timer = setTimeout(() => {
fn.apply(this, arguments)
}, delay) } }
div1.addEventListener('drag', throttle((e) => {
console.log(e.offsetX, e.offsetY) }, 1000))
div1.addEventListener('drag', function(e) { })
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| function throttle3(fn, delay = 100, event) { let timer = null
return function (event) { if (timer) { return } timer = setTimeout(() => { fn.apply(this, [event]) timer = null }, delay) } }
div3.addEventListener('drag', throttle3(function(e) { console.log(e.offsetX, e.offsetY) }, 1000), event)
|
arguments 不包括fn、delay,只有事件对象
- 注意:为啥arguments并不会包括fn、delay,只有事件对象?
- 原因:
- arguments 对象在这里指的是返回函数内部的参数,不包括外部 throttle 函数的参数(fn 和 delay)
- 而箭头函数没有 arguments ,所以指的是父级函数(即 throttle 返回的函数)的 arguments
- 所以返回函数中 arguments 包含了传递给 throttle 返回的函数的参数,而不包括 fn 和 delay 这两个参数
Lodash内置的节流函数
- lodash文档 节流函数,该函数会返回一个新函数,以第一次触发事件为准,在指定时间内最多执行一次,多余的调用都会被忽略
- 提供一个 cancel 方法取消延迟的函数调用,以及 flush 方法立即调用
- 参数1:执行函数func
- 参数2:需要节流的毫秒wait
- 参数3:配置对象 决定如何调用 func 方法
- leading:指定调用在节流开始前(默认true)也就是wait前就先执行一次
- trailing:指定调用在节流结束后(默认true)也就是wait后也先执行一次
- 可参考掘金使用例子
throttle踩雷
- 需要使用event的函数都要谨慎使用节流,容易出现问题,节流应该关注函数和时间而不是事件对象
- 拖拽事件中ondragover不要使用节流,阻止默认行为
event.preventDefault()
这个动作会不生效,会导致目标元素不允许放置被拖动的数据
- 且在处理拖拽操作时,特别是在dragover事件中,阻止默认处理方式并设置允许放置是非常重要的(例子),它保证拖放操作的正确性和流畅性。由于dragover事件在拖拽过程中会频繁触发,使用节流可能会导致延迟和不准确的拖放效果。
- ondragover事件是在拖动对象在某个元素上悬停时触发的。通常,它绑定在目标元素上,表示允许将拖动对象放置在这个目标元素上。在这个事件中,你可以阻止默认的拖动行为,以控制允许或拒绝拖放操作
- 使用节流通常更适合在 drag 事件上而不是 ondragover 事件上。 drag 事件是指元素在拖动期间触发的事件
安全
XSS跨站请求攻击
- XSS:一个博客网站,我发表一篇博客,其中嵌入
<script>
脚本,脚本内容用以获取cookie,发送到我的服务器(只要我将自己的服务器设置好配合跨域,那么点击我的链接我就可以解决cookie跨域的问题)。那么只要我发布这篇博客,有人查看它,我就能轻松收割访问者的cookie。
- 例子:攻击者在博客中嵌入
<script>
脚本来获取访问者的cookie(cookie只要获取到,那么script中还可加入ajax等跨域方式(比如jsonp)将cookie发送出去,此时只要攻击者的服务器设置为支持跨域,那攻击者就能轻松获取数据):!其他用户访问该博客时,cookie被带出:假设script中还有ajax等跨域方式(比如jsonp)将cookie发送出去,此时只要攻击者的服务器设置为支持跨域,则敏感信息也会被送出。
- XSS预防 : 前端在显示时替换,后端在存储时替换,都做总不会有错(可参考XSS网络攻击及防范)
- 替换特殊字符,如
<
变为<;
>
变为>
,<script>
变为<;script>;
直接显示,而不会作为脚本执行(实际工作中可使用XSS工具)
- 例子:特殊字符在html中会被浏览器解析显示为相应的符号,故script以字符串形式显示,不会作为脚本执行
- 在HTTP头部配上HttpOnly(后端通过
set-cookie:httponly
,前端通过document.cookie = "cookieName=cookieValue; HttpOnly";
这样设置),禁止javascript脚本来访问cookie。严格来说,HttpOnly 并非阻止 XSS 攻击,而是能阻止 XSS 攻击后的 Cookie 劫持攻击。(在node中的设置方法可参考博客项目登录(cookie))
- 设置cookie的secure属性为
secure:true
,告诉浏览器仅在请求为https的时候发送cookie
- XSS工具:替换特殊字符可使用XSS工具
- node使用XSS的方法可参考博客:博客项目安全
- 前端使用XSS的方法可参考官方例子:
XSRF跨站请求伪造
- XSRF(CSRF)例子:
- 你正在购物,看中了某个商品,商品id是100,付费接口是
xxx.com/pay?id=100
,但没有任何验证(此时你已经登录了该网站)。
- 我是攻击者,我看中了一个商品,id是200(我现在想让你来帮我买单)
- 此时我向你发送一封电子邮件,邮件标题很吸引人但邮件正文隐藏着
<img src=xxx.com/pay?id=200/>
你一查看邮件,就帮我购买了id是200的商品。(注意img是支持跨域的,此时你的cookie会被带着)
- 预防XSRF:(更多防御方式可参考csrf网络攻击及防范)
- 使用post接口,img这些跨域只能接收get请求,设计post请求的需要server端的允许
- 增加验证,例如密码、短信验证码、指纹等。验证码会强制用户必须与应用进行交互,才能完成最终请求,但是也不能给网站所有的操作都加上验证码,所以只能作为防御 CSRF 的一种辅助手段,而不能作为最终的解决方案。
- 使用token验证:在 HTTP 请求中以参数的形式加入一个随机产生的 token,并在服务器端建立一个拦截器来验证这个 token,若请求无 token 或者 token 不正确,则认为可能是 CSRF 攻击而拒绝该请求。(例子)
- 检查https头部的Referer:在HTTP头中有一个字段叫做Referer,它记录了该HTTP请求的来源地址。通过Referer Check,可以检查是否来自合法的”源”。
例如:从www.user.com
发起的删帖请求,那么Referer值是http://www.user.com
, 删帖请求应该被允许;而如果是从CSRF攻击者构造的页面www.attack.com
发起删帖请求, 那么Referer值是http://www.attack.com
, 删帖请求应该被阻止。
相关题目
下面几个题都涉及解构问题,太长时间不看原生js或者借助vscode一键生成模板可能会有点模糊,复习下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <!DOCTYPE html> <html> <head> <title>DOM Content Loaded Example</title> <link rel="stylesheet" type="text/css" href="styles.css"> </head> <body> <h1>Hello, World!</h1>
<script> document.addEventListener("DOMContentLoaded", function () { const heading = document.querySelector("h1"); heading.textContent = "DOM Content Loaded Example"; }); </script> </body> </html>
|
为什么建议CSS写在head中
因为页面是边解析边渲染的,如果把CSS写在body内(HTML规范允许在 <body>
中使用 <link>
标签),则先解析DOM树渲染在页面上,再生成CSSOM合成渲染树重新渲染在页面上,这样会有一个过程,用户可能会看到一个过程变化。
所以在DOM树生成之前就先生成CSSOM会更好,这样当DOM树生成时就可直接和所有CSSOM进行合并,一步渲染完成。
为什么建议JS写在body最后
因为页面是边解析边渲染的,如果把JS写在body中,虽然JS有异步的处理机制,但极端情况会出现前面已经渲染了一半就卡住的情况。渲染时间会拖长。
从输入url到页面显示都经历了什么
window.onload和DOMContentLoaded的区别
- window.onload:页面的全部资源加载完才会执行,包括图片、视频等
- DOMContentLoaded:DOM渲染完即可执行,此时图片、视频还可能没有加载完
- 注意:使用DOMContentLoaded来触发JS比window.onload更好,不需要等图片、视频都加载完成才触发JS的加载,这样页面渲染会更快。
1 2 3 4 5 6
| window.addEventListener('load', function() { }) document.addEventListener('DOMContentLoaded', function() { })
|
- 什么操作可以放 DOMContentLoaded 中?
- 修改文档元素:您可以使用JavaScript来操作文档中的DOM元素,例如更改文本内容、样式、属性或结构。这通常用于更新页面的初始状态。
- 绑定事件处理程序:您可以在 DOMContentLoaded 事件处理函数中绑定其他事件处理程序,以便用户与页面进行交互。例如,您可以添加点击事件处理程序、表单提交事件处理程序等
- 发起异步请求:如果您需要在页面加载后从服务器获取数据,可以在 DOMContentLoaded 事件处理函数中发起异步请求,以确保数据获取发生在用户与页面交互之前
- 设置初始状态:您可以在这里设置页面的初始状态,例如填充表单字段、初始化UI组件、或执行其他与用户界面相关的初始化工作