前言:
这篇博客是记录自己在看面试过程中还未完全掌握的前端知识点,也是一些前端面试需要掌握的知识点(总结的并不全面,可以参考,具体情况以自己实际为准),并且这篇博客正在持续更新中…
附言:有时候面试还会遇到面试官问一些无关技术的问题,比如聊职业发展规划、为什么选前端、平常怎么自学前端、最近的学习过程中遇到了哪些印象深刻的知识、 如何协调和沟通需求、得到产品原型后如何进行开发工作的划分等等,可以根据自己情况整理一些,面试问道也不至于很乱。
(其余整理的知识点,参照前端面试整理(二))
1、vue写一个sleep函数
下面是一个示例代码:
function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }
这个函数接受一个毫秒数,返回一个Promise对象。调用这个函数后,会等待传入的时间(即休眠)后,Promise对象会被resolve。可以用await
关键字等待Promise解决(即休眠完成),然后再执行下一步操作。
例如,可以这样调用sleep函数:
async function doSomething() { console.log('start'); await sleep(1000); console.log('end'); } doSomething();
上述代码会输出 start
,然后等待1秒钟后才输出 end
。
2、强制缓存协商缓存理解
304 状态码是 HTTP 协议中表示资源未发生变化的状态码,用于优化性能,减少带宽消耗。在处理 HTTP 请求时,服务器会通过判断资源的状态来决定是使用强缓存还是协商缓存。
强制缓存:强制缓存整体流程比较简单,就是在第一次访问服务器取到数据之后,在过期时间之内不会再去重复请求。实现这个流程的核心就是如何知道当前时间是否超过了过期时间。
协商缓存:协商缓存与强制缓存的不同之处在于,协商缓存每次读取数据时都需要跟服务器通信,并且会增加缓存标识。
区别:
(1)触发的先后顺序不同
先去判断文件是否过期(下面会说如何判断是否过期),没过期触发强制缓存,浏览器直接读取本地文件,http状态码200 (from memory cache)
或者 (from disk cache)。
文件已经过期了,触发协商缓存,发起请求询问服务器该文件是否
有更新,没有更新则使用浏览器本地缓存文件,文件有更新则服务器返回新的文件给客户端,且更新新的过期时间并缓存起来。
(2) 强制缓存不访问服务器、协商缓存需要访问服务器
强制缓存
是浏览器 自导自演 的行为,发起请求时看该文件是否过期,没过期直接使用。
协商缓存
是浏览器发现文件过期了,需要和 服务器端通讯 ,让服务器判断是否过期,没过期就还是用浏览器缓存,过期了就用服务器新返回的文件。
过程:
-
1.浏览器第一次加载资源,服务器返回200,浏览器将资源文件从服务器上请求下载下来,并把response header及该请求的返回时间一并缓存;
-
2.下一次加载资源时,先比较当前时间和上一次返回200时的时间差,如果没有超过cache-control设置的max-age,则没有过期,命中强缓存,不发请求直接从本地缓存读取该文件(如果浏览器不支持HTTP1.1,则用expires判断是否过期);如果时间过期,则向服务器发送header带有If-None-Match和If-Modified-Since的请求
-
3.服务器收到请求后,优先根据Etag的值判断被请求的文件有没有做修改,Etag值一致则没有修改,命中协商缓存,返回304;如果不一致则有改动,直接返回新的资源文件带上新的Etag值并返回200;;
-
4.如果服务器收到的请求没有Etag值,则将If-Modified-Since和被请求文件的最后修改时间做比对,一致则命中协商缓存,返回304;不一致则返回新的last-modified和文件并返回200;
3、promisify 函数理解
在 node 中有个 util 模块
const { promisify } = require('util')
作用:将原本需要通过传入回调函数来实现,用promise
的.then
的方式来调用,从而实现逻辑上的同步操作。
promisify
是个函数,参数里面传个函数,promisify
的返回值也是个函数,调用这个函数,这个函数的返回是 promise
对象
例1:
const fs = require('fs')
const path = require('path')
const textPath = path.join(__dirname, '/test.md')
// 读取示例文件
fs.readFile(textPath, 'utf8', (err, contrast) => {
// 通过promisefy转化为链式调用
const readFileSync = promisefy(fs.readFile)
readFileSync(textPath, 'utf8')
.then((res) => {
console.log(res === contrast) // 此处结果预期:true,即promise返回内容与前面读取内容一致
})
.catch((err) => {})
})
const promisefy = (fn) => {
// TODO 此处完成该函数的封装
}
module.exports = promisefy // 请勿删除该行代码
例2:
const { promisify } = require('util')
const fs = require('fs')
const reader = promisify(fs.readFile)
reader('./ww.txt', { encoding: 'utf-8' }).then((res) => {
console.log(res)
}).catch((err) => {
console.log(err)
})
手写 promiseify 看看它内部到底是怎样实现的:
const promisify = (fn) => {
return (...args) => {
return new Promise((resolve, reject) => {
fn(...args, (err, data) => {
if (err) reject(err)
resolve(data)
})
})
}
}
const demo = promisify((a, b, c, cb) => {
console.log(a, b, c)
cb(null, a)
})
demo(1, 2, 3).then((res) => {
console.log(res, 'res')
})
4、部署偏前端
随着互联网技术的发展和普及,Web前端开发已成为当今最重要和最具前景的技能之一。与此同时,如何将 Web 前端部署到服务器上已成为一个必不可少的技能。本文将介绍 Web 前端部署的几种方法和步骤。
一、前置准备
在开始 Web 前端部署之前,需要我们先安装必要的环境和工具,具体如下:
1.服务器环境:为了部署我们的 Web 前端项目,我们首先需要一台服务器,可以选择购买云服务器或自己搭建一台服务器。
2.Web服务器:我们需要安装一个支持 HTTP 请求的 Web 服务器,如 Apache 和 Nginx 等。在部署前端项目时,推荐使用 Nginx。
3.版本控制工具:Git 是常用的版本控制工具,对于团队协作来说是非常必要的。
4.代码编辑器:Sublime Text、VS Code、Atom 等都是非常优秀的编辑器,可以根据个人喜好选择。
二、部署 Web 前端项目
1.简单部署
如果只是一个简单的 Web 前端项目,我们可以直接将代码上传到服务器的指定目录下,并配置好 Apache 或 Nginx 的静态文件目录,使得服务器能够正常访问我们的项目。
步骤如下:
(1)将完整的前端项目文件夹打包成一个压缩文件并上传至服务器。
(2)解压上传的文件,在 Nginx 的配置文件中配置前端项目的访问域名,将前端项目与 Nginx 构建并行。
(3)在 Nginx 的配置文件中配置反向代理,将客户端请求转发到前端项目的访问入口文件 index.html。
(4)重启 Nginx 服务,前端项目就能够成功部署到服务器上。
2.自动化部署
在真实的项目中,我们往往需要频繁地更新我们的代码和文件,这时候手动部署显然不够高效。为此,我们可以使用一些自动化工具来实现自动部署,如 Jenkins、Travis CI 等。
其中,Travis CI 是针对 Github 仓库的持续集成与持续部署工具,可以不断跟踪仓库中的代码提交,一旦有新的提交,就会自动触发构建和部署。
步骤如下:
(1)将前端项目的代码托管在 Github 等代码仓库中。
(2)在 Travis CI 中设置自动化构建和自动化部署的相关脚本。
(3)在 Github 中提交代码,Travis CI 将自动触发构建和部署流程,生成可运行的前端代码并部署到服务器上。
以上是 Web 前端部署的几种方法和步骤,我们可以根据自己的实际需求和项目规模选择适合的部署方式。在实践中,需要注意的是,我们需要选择一个可靠的服务器和稳定的 Web 服务器,以及进行适当的防火墙配置和安全性措施,确保项目的稳定性和安全性。
5、script标签中defer和async的区别
script 标签有2个属性 async(异步) 和 defer(推迟);他们的功能是:
async:他是异步加载,不确定何时会加载好;页面加载时,带有 async 的脚本也同时加载,加载好后会立即执行,如果有一些需要操作 DOM 的脚本加载比较慢时,这样会造成 DOM 还没有加载好,脚本就进行操作,会造成错误。
defer:页面加载时,带有 defer 的脚本也同时加载,加载后会等待 页面加载好后,才执行。
defer解释
这个属性的用途是表明脚本在执行时不会影响页面的构造。也就是说,脚本会被延迟到整个页面都解析完毕后再运行。因此,在<script>元素中设置defer属性,相当于告诉浏览器立即下载,但延迟执行。
HTML5规范要求脚本按照它们出现的先后顺序执行,因此第一个延迟脚本会先于第二个延迟脚本执行,而这两个脚本会先于DOMContentLoaded事件执行。在现实当中,延迟脚本并不一定会按照顺序执行,也不一定会在DOMContentLoad时间触发前执行,因此最好只包含一个延迟脚本。
async解释
这个属性与defer类似,都用于改变处理脚本的行为。同样与defer类似,async只适用于外部脚本文件,并告诉浏览器立即下载文件。但与defer不同的是,标记为async的脚本并不保证按照它们的先后顺序执行。
第二个脚本文件可能会在第一个脚本文件之前执行。因此确保两者之间互不依赖非常重要。指定async属性的目的是不让页面等待两个脚本下载和执行,从而异步加载页面其他内容。
6、细解跨域以及跨域的解决方案
一、同源策略
同源策略是一个重要的安全策略,它用于限制一个Origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。
Origin:指web文档的来源,Web 内容的来源取决于访问的URL的方案 (协议),主机 (域名) 和端口定义。只有当方案,主机和端口都匹配时,两个对象具有相同的起源。
二、跨域
关于URL是否同源,根据上图中的①②③进行判断即可,只要有一点不同,就达到跨域的条件。顺带一提,即便是向域名对应的ip进行资源请求,仍然会跨域。
IE的特殊性:Internet Explorer 的同源策略有两点差异,一是IE未将端口号纳入同源策略的检查,其次是两个高度互信的域名也不受同源策略的检查。
常见的跨域情景:
浏览器内常见的跨域报错:
跨域出现的场景:
一般常见于开发阶段,本地启动项目后,当前页面域名和后台服务器域名不相同,导致跨域。在项目上线后,会通过统一域名、后端配置域名白名单等方式避免跨域。
下方的解决方案中,我们通过koa2框架搭建服务器,实现一系列的情景模拟。
三、跨域的解决方案
1.JSONP
原理:通过script标签没有跨域限制的特性,进行资源的请求和获取。
限制:需要目标服务器进行配合,且仅支持get请求
我们直接通过代码和注释,理解jsonp的使用前端代码如下:
<script>
window.jsonp = function(res){
console.log(res);
}
</script>
<script src="http://localhost:9527/jsonp?val=123&cb=jsonp"></script>
后端代码如下:
// 定义jsonp接口
router.get('/jsonp', async (ctx, next) => {
/*
1.后端通过query获取前端传来的请求参数
其中包括:
· 交予后端进行功能逻辑操作的数据,如val
· 交予后端进行jsonp操作的函数名,如cb
*/
const {cb, val} = ctx.query
// 2.调用回调函数,进行传参,将处理好的数据返回给前端
if(val === '123'){
const requestData = {
code: 10001,
data: '登陆成功'
}
//在响应体中触发目标函数,并将处理好的数据requestData作为实参传入
ctx.body = `${cb}(${JSON.stringify(requestData)})`;
}
})
前端通过window对象,在全局挂载了一个待触发的函数。
后端通过响应体触发这个函数,并将数据作为入参,传给前端。
了解简单的实现后,前端可以对jsonp的功能再进行一层封装:
/*
1. 生成script标签,我们需要script标签进行接口的调用
2. 处理参数数据,分别整理好接口,接口参数,函数名等数据,并进行填充
3. 写入生成好的script标签,实现接口的调用(返回promise对象,便于链式调用)
4. 清除script标签
*/
function jsonp(requestData) {
// 对传入参数进行处理
const { url, data, jsonp } = requestData;
let query = '';
for (let key in data) {
query += `${key}=${data[key]}&`;
}
const src = `${url}?${query}jsonp=${jsonp}`;
// 生成、填充script标签,在页面中挂载调用接口
let scriptTag = document.createElement('script');
scriptTag.src = src;
document.body.appendChild(scriptTag);
return new Promise((resolve, reject) => {
window[jsonp] = function(rest){
resolve(rest)
document.body.removeChild(scriptTag)
}
})
}
// 整理数据
const requestData = {
url: 'http://localhost:9527/jsonp',
data: {
val: 123,
},
jsonp: 'getMessage'
}
// 接口调用
btn.onclick = function () {
jsonp(requestData).then(function (response) {
console.log(response);
})
}
2.CORS
Cross-Origin Resource sharing(跨域资源共享),是一种基于HTTP头的机制,该机制允许服务器标示除了它自己以外其他origin(域名,协议和端口),既浏览器在跨域的情景下仍然能从目标服务器请求并获取资源。
而对服务器数据可能产生副作用的HTTP请求方法,都会触发CORS中的预检机制。
CORS中通过预检机制(preflight request)检查服务器是否允许浏览器发送真实请求,浏览器会先发送一个预检请求(option请求),请求中会携带真实请求的请求信息:
origin:请求的来源
Access-Control-Request-Method:
通知服务器在真正的请求中会采用哪种HTTP方法(GET,POST,DELETE...)
Access-Control-Request-Headers:通知服务器在真正的请求中会采用哪些请求头
服务器可以在预检请求中,可以根据以上三条信息,确定预检请求是否通过:
//server.js
app.use(async (ctx, next) => {
// 允许跨域资源共享的白名单
const whiteList = ['http://127.0.0.1:5500']
// 判断目标源是否通行
const pass = whiteList.includes(ctx.header.origin)
// 对于预检请求,如果没有设置正确的响应状态,浏览器会直接拦截真实请求,直接报错提示跨域
// 所以我们可以在这一部分,确定客户端的请求是否符合我们的要求
if (ctx.method === "OPTIONS") {
if (!pass) return
// 预检放行
ctx.status = 204
}
await next();
});
响应的状态码是决定预检请求是否通过的关键,返回正常的状态码(通常是204)就能通过预检请求,让浏览器发出真实的请求。
在代码中也可以看出,pass是决定预检请求的关键,那在实际的项目中,还得根据设计去决定通行的具体条件。当通过预检请求后,后台可以设置对应的响应头数据,例如是否允许目标源跨域资源共享:
//server.js
app.use(async (ctx, next) => {
console.log('middleware for cors');
// 允许跨域资源共享的白名单
const whiteList = ['http://127.0.0.1:5500']
// 判断目标源是否通行
const pass = whiteList.includes(ctx.header.origin)
// 对于预检请求,如果没有设置正确的响应状态,浏览器会直接拦截真实请求,直接报错跨域
// 所以我们可以在这一部分,确定客户端的请求是否符合我们的要求
if (ctx.method === "OPTIONS") {
if (!pass) return
// 预检放行
ctx.status = 204
}
// 允许访问的origin
ctx.set("Access-Control-Allow-Origin", ctx.headers.origin);
// cookie是否允许携带
ctx.set("Access-Control-Allow-Credentials", true);
// 允许访问的HTTP方法
ctx.set("Access-Control-Request-Method", "PUT,POST,GET,DELETE,OPTIONS");
// 哪些请求头允许通行
ctx.set(
"Access-Control-Allow-Headers",
"X-Requested-With,Content-Type,Accept,Origin"
);
// 暴露给客户端的响应头信息,在不设置的情况下,客户端只能获取默认的响应头,如’content-type‘
ctx.set(
"Access-Control-Expose-Headers",
"With-Requested-Key"
);
// 设置对应的响应头数据
ctx.set(
"With-Requested-Key",
"HW"
);
// 预检结果的缓存时间,毫秒为单位,Firefox上限是86400-24小时,Chromium(谷歌引擎)上限是7200-2小时
ctx.set("Access-Control-Max-Age", 0);
await next();
});
其中需要注意两个点:
关于Access-Control-Expose-Header
使用CORS时,浏览器只允许获取默认的响应头,像上文代码中的标头With-Requested-Key,即便我们可以通过浏览器的调试器查看,也无法通过代码去获取,这时候就需要后台通过Access-Control-Expose-Header进行暴露(后台代码在已在上方统一贴出)。
前端代码
<body>
<button id="btn"> 请求资源 </button>
</body>
<script>
btn.onclick = function () {
axios.post('http://localhost:9527/getMessage', {
firstName: 'Fred',
lastName: 'Flintstone'
})
.then(function (response) {
// 可以在里面查找到暴露出来的响应头数据,如’With-Requested-Key‘: "HW"
console.log(response.headers);
})
.catch(function (error) {
console.log(error);
});
}
</script>
关于Access-Control-Allow-Credentials
使用CORS时,默认不携带cookie,需要同时满足三个条件,才能在使用CORS时进行cookie的传递:
浏览器的请求中,设置withCredentials参数为true
服务端设置标头Access-Control-Allow-Credentials为true
服务端设置标头Access-Control-Allow-Origin不为*
我们可以在原生ajax请求中设置该参数,或者在axios的默认配置中设置该参数:
// 原生ajax
const xhr = new XMLHttpRequest()
xhr.withCredentials = true
// axios
axios.defaults.withCredentials = true;
Ok,明白CORS的作用,以及明白CORS中的预检机制后,接下来是了解什么时机下会触发预检机制。
CORS中归纳了一系列不会触发预检机制的请求场景,即满足所有下述条件的情况下,统称为简单请求:
使用这三种方法之一:GET HEAD POST
不得人为设置此集合外的其他首部字段:Accept Accept-Language Content-Language Content-Type
Content-type的值仅限于这三者之一:
text/plain
multipart/form-data
application/x-www/form-urlencoded
请求中,XMLHttpRequest实例没有注册任何事件监听器,即XMLHttpRequest实例对象可以使用XMLHttpRequest.upload属性进行访问
请求中没有使用ReadableStream对象
小结:CORS中主要区分了简单请求和复杂请求两种情况,复杂请求会触发CORS的预检机制。通过上方的案例,也可以清楚CORS的配置主要是在服务端,但客户端也需要知道CORS的使用注意点,例如响应头数据的获取以及cookies的携带配置,这些知识应该是前后端都需要掌握的技能点。
3.服务器代理
同源策略主要是限制浏览器和服务器之间的请求,服务器与服务器之间并不存在跨域。
我们可以通过koa2模拟和实现这种概念:
//前端代码
<body>
<button id="btn"> 请求资源 </button>
<script>
btn.onclick = function () {
let url = checkUrlProxy('http://localhost:9527/api/getMessage','api')
axios.post(url, {
firstName: 'Fred',
lastName: 'Flintstone'
})
.then(function (response) {
console.log(response);
})
}
// 判断接口是否携带api字段,若是,则更改为代理服务器对应的域名
function checkUrlProxy(url, proxyFlag){
let proxyServer = 'http://localhost:1005'
let urlArr = [url.split('/')[1],url.split('/')[3]]
if(urlArr.includes(proxyFlag)) {
return `${proxyServer}/${proxyFlag}${url.split(proxyFlag)[1]}`
}
return url
}
//
</script>
</body>
前端的代码部分,通过checkUrlProxy函数简单地确定本次请求是否要转向代理服务器。
后端代码如下:
//proxyServer.js
let requestFlag = false
let body = ''
app.use(async (ctx, next) => {
// 全放行
if (ctx.method === "OPTIONS") {
ctx.status = 204
requestFlag = false
} else {
requestFlag = true
}
ctx.set("Access-Control-Allow-Origin", "*");
ctx.set("Access-Control-Allow-Credentials", true);
ctx.set("Access-Control-Request-Method", "*");
ctx.set(
"Access-Control-Allow-Headers",
"X-Requested-With,Content-Type,Accept,Origin"
);
ctx.set("Access-Control-Max-Age", 86400);
// 根据具体情况进行修改
ctx.set("Access-Control-Expose-Headers", "With-Requested-Key");
await next();
if(requestFlag) {
ctx.body = body
body = ''
}
});
app.use(async (ctx, next) => {
if (!requestFlag) return
await p4r(ctx)
});
function p4r(ctx) {
return new Promise((res, rej) => {
const proxyRequest = http.request({
host: '127.0.0.1',
port: 9527,
path: ctx.url,
method: ctx.method,
headers: ctx.header
},
serverResponse => {
serverResponse.on('data', chunk => {
body += chunk
})
serverResponse.on('end', () => {
res(body)
})
}
)
proxyRequest.end()
})
}
app.on('error', (err, ctx) => {
console.error('server error', err, ctx)
});
app.listen(1005, (err) => {
if (err) console.log('服务器启动失败');
else console.log('proxy server 1005 running --> ✨✨✨');
})
//targetServer.js
const data = {val : 123}
// 配合代理服务器的post路由
router.post('/api/getMessage', (ctx) => {
ctx.body = JSON.stringify(data)
})
// 定义好路由组件的内容后进行路由注册
app.use(router.routes())
app.on('error', (err, ctx) => {
console.error('server error', err, ctx)
});
app.listen(9527, (err) => {
if (err) console.log('服务器启动失败');
else console.log('服务器启动成功');
})
后端代码主要分两部分:
代理服务器(proxyServer),代理服务器设置CORS时不限制通行,在koa2框架中,通过中间件向目标服务器发送请求,当接收到对应数据后,再响应给浏览器
目标服务器(targetServer),目标服务器不需要做太复杂的配置,案例中只是将数据传递给请求方
Ok,我们通过这个案例,明确代理服务器的具体效果,浏览器向目标服务器直接请求资源,仍然会受到同源策略的影响,但通过代理服务器向目标服务器请求资源时,却没这种限制。
那在实际项目中,我们可以通过脚手架或打包工具的配置文件,简洁方便地设置代理服务器,无需自己手写服务器代码,拿vue的脚手架为例:
devServer:{
proxy:{
'api':{
target:'127.0.0.1:9527', //目标服务器地址
changeOrigin: true, // 是否允许跨域
pathRewrite: { //是否重写接口
'api':'',
}
}
}
}
在配置的时候,可以通过框架的脚手架,或者打包工具确定配置文件,例如一些熟悉的字眼:vue.config.jswebpack.config.jspackage.json(react),更准确的做法就是直接去对应工具的官方文档查阅代理服务器的配置介绍。
CommonJS require/import的区别
// 如果第一个require没有执行结束,后面代码不会执行(会产生阻塞)
const {obj1} = require('./a')
const {obj2} = require('./b')
const {obj3} = require('./c')// import是异步加载,谁先加载完就会先执行谁(不会产生阻塞)
import {obj1} from './a'
import {obj2} from './b'
import {obj3} from './c'
require是运行时编译,import是编译阶段执行,在代码执行前
// 这里会报错,因为这里要先require引入之后才能够获取到obj1,这就是运行时加载
console.log(obj1);
const {obj1} = require('./a')
1
// 这里可以正常输出,是因为代码先进行了编译,所以不会出现错误
console.log(obj2);
import {obj2} from './b'
7、事件循环机制(Event Loop) 也可以理解为:不断地从任务队列中取出任务执行的一个过程
整体会把所有代码分为两个部分:‘同步任务’,‘异步任务’
所有同步任务都在主线程上执行,形成一个执行栈
主线程之外还存在一个任务队列,专门存放异步任务(宏任务和微任务)
宏任务进入到Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中
微任务也会进入到另一个Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会讲这个函数移到Event Queue中
整体script作为第一个宏任务进入主线程,当主线程的任务执行完毕,主线程为空时,会检查微任务的Event Queue,如果有任务,就会全部执行,如果没有就执行下一个宏任务
主线程不断重复上面的步骤,这就是Event Loop事件循环,只要主线程空了,就会去读取"任务队列"。这个过程会不断重复。
8、判断数据类型
typeof可以区分一部分数据类型,结果如下:
typeof 123 //Number typeof 'abc' //String typeof true //Boolean typeof undefined //Undefined typeof Symbol() //Symbol typeof null //Object typeof { } //Object typeof [ ] //Object typeof console.log() //Function
(2)、判断null、object和array
从上面的结果可以看出,用typeof检测 null、数组、对象的结果都是Object,所以需要用其他方法区分他们的类型。
判断null 可以用===null来判断。
判断object和array
① isArray
Array.isArray([]) //true Array.isArray({}) //false
② instanceof
[] instanceof Array //true {} instanceof Array //false
③ constructor
{}.constructor //返回object [].constructor //返回Array
④ Object.prototype.toString.call
Object.prototype.toString.call([]) //["object Array"] Object.prototype.toString.call({}) //["object Object"]
9、模拟私有变量的实现
10、浏览器输入url到客户收到数据的过程
在浏览器中输入 URL 之后,它会执行以下几个流程:
执行 DNS 域名解析;
封装 HTTP 请求数据包;
封装 TCP 请求数据包;
建立 TCP 连接(3 次握手);
参数从客户端传递到服务器端;
服务器端得到客户端参数之后,进行相应的业务处理,再将结果封装成 HTTP 包,返回给客户端;
服务器端和客户端的交互完成,断开 TCP 连接(4 次挥手);
浏览器通过自身执行引擎,渲染并展示最终结果给用户。
1.DNS 域名解析
在网络中定位是依靠 IP 进行身份定位的,所以 URL 访问的第一步便是先要得到服务器端的 IP 地址。而得到服务器的 IP 地址需要使用 DNS(Domain Name System,域名系统)域名解析,DNS 域名解析就是通过 URL 找到与之相对应的 IP 地址。
PS:为什么不直接访问 IP 地址来请求服务器?因为 IP 地址很长,不方便记忆,而 URL 地址好记很多,所以会使用 URL 来替代 IP 地址,而 URL 就像 IP 地址的别名一样,用它可以定位到相应的 IP 地址。
DNS 域名解析的大致流程如下:
先检查浏览器中的 DNS 缓存,如果浏览器中有对应的记录会直接使用,并完成解析;
如果浏览器没有缓存,那就去查询操作系统的缓存,如果查询到记录就可以直接返回 IP 地址,完成解析;
如果操作系统没有 DNS 缓存,就会去查看本地 host 文件,Windows 操作系统下,host 文件一般位于 "C:\Windows\System32\drivers\etc\hosts",如果 host 文件有记录则直接使用;
如果本地 host 文件没有相应的记录,会请求本地 DNS 服务器,本地 DNS 服务器一般是由本地网络服务商如移动、电信提供。通常情况下可通过 DHCP 自动分配,当然你也可以自己手动配置。目前用的比较多的是谷歌提供的公用 DNS 是 8.8.8.8 和国内的公用 DNS 是 114.114.114.114。
如果本地 DNS 服务器没有相应的记录,就会去根域名服务器查询了,目前全球一共有 13 组根域名服务器(这里并不是指 13 台服务器,是指 13 个 ip 地址,按字母 a-m 编号),为了能更高效完成全球所有域名的解析请求,根域名服务器本身并不会直接去解析域名,而是会把不同的解析请求分配给下面的其他服务器去完成,下面是 DNS 域名系统的树状结构图:
2.封装 HTTP 请求数据包
一个 HTTP 请求对象包含 4 部分内容:
请求行
请求报头
空行
请求正文
3.封装 TCP 请求数据包
HTTP 底层是依赖 TCP/IP 协议实现的,所以在底层数据传输时,会将 HTTP 请求包进一步封装成 TCP 数据包。
4.建立 TCP 连接(3 次握手)
HTTP 通讯的基础是 TCP 连接,TCP 连接需要 3 次握手,3 次握手就是为了验证客户端的发送能力和接收能力,以及服务器端的发生能力和接收能力,就像打电话一样,通常的通话是这样开头的:
我:喂,能听到吗?
对方:能听到,你能听到吗?(证明了对方的接收能力和我的发送能力)
我:我也能听到,咱们聊正事吧。(证明了对方的发送能力和我的接收能力)
经过以上 3 次握手就可以证明客户端的发送能力和接收能力,以及服务器端的发生能力和接收能力,这样就可以正式开始通讯了。
5.服务器端获取到 HTTP 请求参数
数据在经过 TCP 传到到服务器程序之后,又会将 TCP 的数据包转换成 HTTP 数据包(这一切都是 TCP/IP 协议的功劳),这样服务器端就可以得到客户端发送的请求数据了。
6.服务器端执行业务处理,并返回数据
服务器端拿到了客户端的请求参数之后,会进行相应的业务处理,处理完成之后,再将处理的结果返回给客户端。返回的流程和发送的流程类似,先将结果封装成 HTTP 数据包,HTTP 数据包可分为以下 4 部分:
状态行
响应报头
空行
响应正文
状态行用于描述服务器的返回状态,它由 3 部分组成:
HTTP 版本号,如 HTTP/1.1;
状态码,如 200;
状态描述信息,如 OK;
常见的状态码有以下几个:
200:返回成功;
301:永久重定向;
302:临时重定向;
404:未找到页面;
500:服务器程序出错。
响应正文就是返回给客户端的所有数据。
7.断开 TCP 连接(4 次挥手)
在经过一次请求和一次响应之后,客户端和服务器的“交流”就结束了,此时就可以执行 TCP 连接断开的流程了,它需要 4 次挥手:
客户端:咱们分手吧;
服务器端:好的,让我准备一下。
服务器端:我准备好了,分手吧。
客户端:好的。
经过了以上流程之后,TCP 的连接就断开了。
8.浏览器渲染并展示结果
经过 TCP 交互之后,客户端也得到了服务器端返回的数据,然后使用浏览器自身的执行引擎,将最终的结果展示给用户,整个执行流程就结束了。
11.常见网络攻击及防御方法总结(XSS、SQL注入、CSRF攻击
参考: http://t.csdnimg.cn/MFTVS
12、七层网络协议的理解
1. 第七层——应用层(application layer)
应用层(application layer):直接为用户的应用进程提供服务,并规定应用程序中通信相关的细节。
在因特网中的应用层协议很多,如支持万维网应用的HTTP
协议,支持电子邮件的SMTP
协议,支持文件传送的FTP
协议,DNS,POP3,SNMP,Telnet等等。
(1):超文本传输协议HTTP:这是一种最基本的客户机/服务器的访问协议;浏览器向服务器发送请求,
而服务器回应相应的网页
(2):文件传送协议FTP:提供交互式的访问,基于客户服务器模式,面向连接 使用TCP可靠的运输服务
主要功能:减少/消除不同操作系统下文件的不兼容性
(3):远程登录协议TELNET:客户服务器模式,能适应许多计算机和操作系统的差异,网络虚拟终端NVT的意义
(4):简单邮件传送协议SMTP:Client/Server模式,面向连接
基本功能:写信、传送、报告传送情况、显示信件、接收方处理信件
(5):DNS域名解析协议:DNS是一种用以将域名转换为IP地址的Internet服务
- 2.第六层——表示层
将 应用处理的信息
转换为 网络标准传输
的格式,
或将来自下一层的数据转换为上层能够处理的格式;
主要负责数据格式的转换,确保一个系统的应用层信息可被另一个系统应用层读取。
具体来说,就是将设备固有的数据格式转换为网络标准传输格式,不同设备对同一比特流解释的结果可能会不同;因此,主要负责使它们保持一致。
3. 第五层——会话层
负责建立和断开通信连接(数据流动的逻辑通路)。
4. 第四层——传输层(transport layer)
运输层(transport layer):负责向两个主机中进程之间的通信提供服务。
由于一个主机可同时运行多个进程,因此运输层有复用和分用
的功能。
复用,就是多个应用层进程可同时使用下面运输层的服务。
分用,就是把收到的信息分别交付给上面应用层中相应的进程。
运输层主要使用以下两种协议:
(1) 传输控制协议TCP(Transmission Control Protocol):有连接的,数据传输的单位是报文段,能够提供可靠的交付。
(2) 用户数据包协议UDP(User Datagram Protocol):无连接的,数据传输的单位是用户数据报,不保证提供可靠的交付,只能提供“尽最大努力交付”。
5.七层理解
物理层:物理接口规范,传输比特流,网卡是工作在物理层的。
数据层:成帧,保证帧的无误传输,MAC地址,形成EHTHERNET帧
网络层:路由选择,流量控制,IP地址,形成IP包
传输层:端口地址,如HTTP对应80端口。TCP和UDP工作于该层,还有就是差错校验和流量控制。
会话层:组织两个会话进程之间的通信,并管理数据的交换使用NETBIOS和WINSOCK协议。QQ等软件进行通讯因该是工作在会话层的。
表示层:使得不同操作系统之间通信成为可能。
应用层:对应于各个应用软件,应用程序。
数据中心由大型服务器、存储以及计算机网络构成(某些大型数据中心甚至连接到“主干网”)
数据中心结构图:
13、instanceof判断
判断是否是其原型链上的实例只要这个构造函数在原型链都返回true
(由Array创建的,Array是 Object的子类,instanceofArray和 Object 都返true)
[]instanceof Array true
[]instanceof Object true
{}instanceof Array false
{}instanceof Object true
construtor
判断实例对象构造函数
[].constructor === Array true
14、原型跟原型链的理解
(原型链是通过哪一个属性进行连接的,怎么从一个实例对象找到它的构造函数)
参考: 原型和原型链的理解-CSDN博客
15、for循环内闭包使用
for(var i = 0; i < 10; i++) {
setTimeout(() => {
console.log(i)
}, 0)
}
答案: 10个10
若要输出从0到9,怎么办?
那么这道题目的详细解答是怎么样的呢?
解析如下:
为什么上面的代码输出的是10个10呢,而不是0到9呢?
具体原因:
因为var关键字声明的标识符的作用域范围是函数作用域。
因此在上面的js代码中,i标识符是存放在全局作用域中的。因此,当setTimeout的回调函数执行的时候,i标识符存放的值已经是5了(5是循环结束的终点),因此将输出5个5。
我们可以仔细阅读一下这个代码,我们发现for循环是同步代码,而setTimeout是异步代码
因此js引擎在执行这个for循环的时候,遇到了setTimeout,将会将setTimeout放入宏任务队列,之后接着执行同步代码,当同步代码执行完毕后去检查微任务队列,在下一轮事件循环开始的时候才会将setTimeout从宏任务队列中取出。
因此,setTimeout的执行必然在所有同步代码,也就是整个for循环执行完毕之后,因此在setTimeout执行的时候,i已经是5了
解决方法:
A.将var改为let
let 是ES6标准的具有块级作用域的声明,let=0时,形成的作用域是独立的,后面的function里的i不会被主线程里的宏任务的i=3所覆盖。
因为var声明编译和执行是分开的,let却只有在进入执行环境时才会被执行。
for(let i = 0;i < 5;i++)
{
setTimeout(() => console.log(i),1000);
}
B.使用闭包
// 使用闭包
for(var i = 0; i < 10; i++) {
(function (i) {
setTimeout(() => {
console.log(i)
}, 0);
})(i);}
16、addEventListener的3个参数
1.addEventListener(),接收3个参数
第一个参数event:监听的事件
第二个参数是函数:需要执行的事
第三个参数是useCapture(变量):用来判断是捕获还是冒泡
2.第三个参数userCapyure
(1)当useCapture为true的时候是在捕获阶段触发事件 (捕获事件触发顺序是由父到子)
(2)当useCapture为false的时候是在冒泡阶段触发事件(默认为false)(冒泡事件触发顺序是由子到父)
17、vue中history和hash的区别是什么
-
1.hash路由在地址栏URL上有#,用 window.location.hash 读取。而history路由没有会好看一点
-
2.我们进行回车刷新操作,hash路由会加载到地址栏对应的页面,而history路由一般就404报错了(刷新是网络请求,没有后端准备时会报错)。
-
3.hash路由支持低版本的浏览器,而history路由是HTML5新增的API。
-
4.hash的特点在于它虽然出现在了URL中,但是不包括在http请求中,所以对于后端是没有一点影响的,所以改变hash不会重新加载页面,所以这也是单页面应用的必备。
-
5.history运用了浏览器的历史记录栈,之前有back,forward,go方法,之后在HTML5中新增了pushState()和replaceState()方法,它们提供了对历史记录进行修改的功能,不过在进行修改时,虽然改变了当前的URL,但是浏览器不会马上向后端发送请求。
-
6.history的这种模式需要后台配置支持将所有访问都指向index.html。比如:当我们进行项目的主页的时候,一切正常,可以访问,但是当我们刷新页面或者直接访问路径的时候就会返回404,那是因为在history模式下,只是动态的通过js操作window.history来改变浏览器地址栏里的路径,并没有发起http请求,但是当我直接在浏览器里输入这个地址的时候,就一定要对服务器发起http请求,但是这个目标在服务器上又不存在,所以会返回404
18、Vue响应式原理
1.通过Object.defineProperty
来实现监听数据的改变和读取(属性中的getter和setter方法) 实现数据劫持,
这样我们就可以在数据被修改时在setter方法设置监视修改页面信息,也就是说每当数据被修改,就会触发对应的set方法,然后我们可以在set方法中去调用操作dom的方法。
vue实现数据响应式,是通过数据劫持侦测数据变化,发布订阅模式进行依赖收集与视图更新,换句话说是Observe,Watcher以及Compile三者相互配合,
Observe实现数据劫持,递归给对象属性,绑定setter和getter函数,属性改变时,通知订阅者
Compile解析模板,把模板中变量换成数据,绑定更新函数,添加订阅者,收到通知就执行更新函数
Watcher作为Observe和Compile中间的桥梁,订阅Observe属性变化的消息,触发Compile更新函数
19、webpack作用
Webpack是一个前端打包工具,主要用于将多个JavaScript、CSS、HTML等文件进行打包和优化,以便于在浏览器中高效地加载和运行。
具体来说,Webpack可以完成以下几个任务:
- 模块打包:Webpack可以将多个JavaScript文件打包成一个或多个输出文件,减少了网络请求次数,提高了网页加载速度。同时,Webpack还支持将其他类型的文件,如CSS、图片、字体等文件,作为模块进行打包。
- 模块转换:Webpack可以使用各种加载器(loader)将不同类型的文件转换为JavaScript模块,以便Webpack打包。
- 代码分割:Webpack支持将代码分割成多个块(chunk),实现按需加载,提高了网页的加载速度。同时,Webpack还支持动态加载代码块,实现更灵活的加载策略。
- 模块热替换:Webpack支持模块热替换(HMR),可以在开发过程中实现实时预览和调试。
- 优化输出:Webpack可以对输出文件进行优化,如压缩代码、提取公共代码等,以减少输出文件的大小,提高网页加载速度。
20、localStorage和sessionStorage的区别
区别如下:
1、localStorage和sessionStorage一样都是用来存储客户端临时信息的对象。
2、localStorage生命周期是永久,这意味着除非用户显示在浏览器提供的UI上清除localStorage信息,否则这些信息将永远存在。
4、sessionStorage生命周期为当前窗口或标签页,一旦窗口或标签页被永久关闭了,那么所有通过sessionStorage存储的数据也就被清空了。
5、不同浏览器无法共享localStorage或sessionStorage中的信息。相同浏览器的不同页面间可以共享相同的 localStorage(页面属于相同域名和端口),但是不同页面或标签页间无法共享sessionStorage的信息。这里需要注意的是,页面及标 签页仅指顶级窗口,如果一个标签页包含多个iframe标签且他们属于同源页面,那么他们之间是可以共享sessionStorage的。