面试:浏览器相关
文章目录
- 浏览器
- 浏览器多线程
- 浏览器请求网页的过程
- 浏览器如何解析HTML
- DOM 树 和 渲染树(render tree) 的区别
- 浏览器如何解析css选择器
- 重绘与重排
- 性能优化:避免重绘与回流
- 前端安全
- XSS(Cross Site Script)
- 什么是XSS
- 分类
- 1、反射型XSS
- 2、存储型XSS
- 3、DOM XSS
- 危害
- 预防
- HTML编码
- HTML Attribute编码
- javaScript编码
- URL 编码
- CSS 编码
- 开启CSP网页安全政策
- SQL注入
- 什么是SQL注入
- 防范
- [CSRF(Cross-site request forgery)](https://blog.csdn.net/stpeace/article/details/53512283)
- CSRF攻击原理及过程
- CSRF防范
- (1)验证 HTTP Referer 字段,检查网页来源
- origin属性
- (2)在请求地址中添加 token 并验证
- (3)在 HTTP 头中自定义属性并验证
- Cookie/Token/session/区别/交互
- Cookie
- 作用:区分客户端/保存登陆凭证
- 存储位置
- 什么时候携带
- 同域/跨域ajax请求到底会不会带上cookie?
- Cookie 的同源和同站
- Token
- 作用:访问资源接口(API)时所需要的资源凭证
- 特点:
- token 的身份验证流程:
- [JWT(Json Web Token)](http://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html)
- refresh token
- Session
- 存储在服务器/记录会话状态/区分用户
- session 认证流程:
- Cookie 和 Session 的区别
- Token 和 Session 的区别
- Token 和 JWT 的区别
- token对于session/cookie有什么优点和缺点
- [Cookie、LocalStorage 与 SessionStorage的区别在哪里?](https://www.cnblogs.com/TigerZhang-home/p/8665348.html)
- 浏览器缓存
- 登陆的业务逻辑设计
- 防止JS获取cookie:http-only、secure-only、host-only
- URL输入以后到渲染发生了什么
- get/post请求属于TCP还是UDP
- get/post的区别是什么
- 跨域
- 什么是跨域
- 同源策略
- 同站/同源
- 响应头字段
- 解决方案
- CORS
- JSONP
- Nginx反向代理
- 其它跨域⽅案
- 跨域的登录态是怎么保持的
- 扫码登录如何实现
- 前端发送请求的方法总结
- 验证码实现
- 浏览器大量http请求怎么优化
- CSS/JS文件加载是否阻塞DOM解析/渲染
- xhr与fetch
- XMLHttpRequest
- Fetch 优点
- Fetch的缺点
- 原生JS操作DOM方法
- DOM 创建
- DOM 查询
- DOM 更改
- 属性操作
- 前端缓存
- HTTP缓存机制
- 强缓存
- 为什么出现Cache-Control
- 协商缓存
- `Cache-Control` 与 `Expires` 的优先级:
- Etag生成
- 离线缓存
- 概念和优势
- 离线缓存的优缺点
- 如何使用
- HTML5存储类型
- web storage
- 离线缓存(application cache)
- Web SQL
- IndexedDB
- Service Worker
- memory cache 和 disk cache
- 常⻅的浏览器内核有哪些
- 前端如何实现即时通讯
- AJAX原理
- window.onload DOMContentLoaded区别
- 单点登陆原理
- 考虑浏览器
浏览器
浏览器多线程

浏览器内核是多线程,在内核控制下各线程相互配合以保持同步,一个浏览器通常由以下常驻线程组成:
- GUI 渲染线程
- JavaScript引擎线程
- 事件触发线程
- 定时触发器线程
- 异步http请求线程
-
GUI渲染线程
GUI渲染线程负责渲染浏览器界面HTML元素,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。
当界面需要重绘(Repaint)或由于某种操作引发回流(重排)(reflow)时,该线程就会执行。
在Javascript引擎运行脚本期间,GUI渲染线程都是处于挂起状态的,也就是说被”冻结”了,GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。 -
JavaScript引擎线程
JavaScript引擎,也可以称为JS内核,主要负责处理Javascript脚本程序,例如V8引擎。
JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序(单线程)。
注意⚠️:GUI渲染线程和JavaScript引擎线程互斥!
原因:由于JavaScript是可操纵DOM的,如果在修改这些元素属性同时渲染界面(即JavaScript线程和UI线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。
因此为了防止渲染出现不可预期的结果,浏览器设置GUI渲染线程与JavaScript引擎为互斥的关系,当JavaScript引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到引擎线程空闲时立即被执行。
如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉。 -
事件触发线程
当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理。
这些事件可以是当前执行的代码块,如定时任务;也可来自浏览器内核的其他线程如鼠标点击、AJAX异步请求等,但由于JS的单线程关系所有这些事件都得排队等待JS引擎处理。 -
定时触发器线程
setInterval与setTimeout所在线程
浏览器定时计数器并不是由JavaScript引擎计数的, 因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确。
通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)
注意,W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。 -
异步http请求线程
在XMLHttpRequest在连接后是通过浏览器新开一个线程请求。
当检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中,再由JavaScript引擎执行。
浏览器请求网页的过程
从输入页面地址到展示页面信息都发生了些什么?
浏览器加载网页时的过程是什么?- 豆皮范儿的回答 - 知乎
https://www.zhihu.com/question/30218438/answer/1644739385
- DNS解析,查找域名服务器的IP地址,先查看缓存,如果没有再进行递归查询
- DNS缓存: 浏览器缓存,系统缓存,路由器缓存,ISP服务器缓存,根域名服务器缓存,顶级域名服务器缓存,主域名服务器缓存。通过缓存直接读取域名相对应的ip,减去了繁琐的查找ip的步骤,大大加快访问速度。
-
获得IP地址后,进行tcp连接,三次握手
-
HTTP发起请求

-
从服务器接收请求到对应后台接收到请求,服务器返回浏览器所请求的资源
服务端接收到请求时,内部会有很多处理:
- 负载均衡(nginx)
- 后台处理
-
浏览器获取资源后,进行解析并渲染页面

-
关闭TCP连接(四次挥手)
浏览器如何解析HTML

从上面这个图上,我们可以看到,浏览器渲染过程如下:
- 解析HTML,生成DOM树(并行请求 css/image/js),解析CSS,生成CSSOM树
- 将DOM树和CSSOM树结合,生成渲染树(Render Tree)
- Layout/reflow(回流):根据生成的渲染树,进行回流(Layout/reflow),得到节点的几何信息(位置,大小)
- Painting(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素
- Display:将像素发送给GPU,展示在页面上

图中重要的四个步骤就是:
(1)计算CSS样式 ;
(2)构建渲染树 ;
(3)布局,主要定位坐标和大小,是否换行,各种position overflow z-index属性 ;
(4)绘制,将图像绘制出来。
1)Layout,也称为Reflow,即回流。一般意味着元素的内容、结构、位置或尺寸发生了变化,需要重新计算样式和渲染树。
2)Repaint,即重绘。意味着元素发生的改变只是影响了元素的一些外观之类的时候(例如,背景色,边框颜色,文字颜色等),此时只需要应用新样式绘制这个元素就可以了。
回流的成本开销要高于重绘,而且一个节点的回流往往回导致子节点以及同级节点的回流, 所以优化方案中一般都包括,尽量避免回流。
DOM 树 和 渲染树(render tree) 的区别
- DOM 树与 HTML 标签一一对应,包括 head 和隐藏元素
- 渲染树不包括 head 和隐藏元素,大段文本的每一个行都是独立节点,每一个节点都有对应的 css 属性

- 在CSS文档中,也同样拥有节点,它的节点与DOM树中的节点是对应的。

为了构建渲染树,浏览器主要完成了以下工作:
- 从DOM树的根节点开始遍历每个可见节点。
- 对于每个可见的节点,找到CSSOM树中对应的规则,并应用它们。
- 根据每个可见节点以及其对应的样式,组合生成渲染树。
浏览器如何解析css选择器
浏览器会**『从右往左』**解析CSS选择器。
从右往左匹配性能更好,是因为从右向左的匹配在第⼀步就筛选掉了⼤量的不符合条件的最右节点(叶⼦节点);⽽从左向右的匹配规则的性能都浪费在了失败的查找上⾯。
重绘与重排
-
当render tree中的一部分(或全部)因为元素的规模尺寸,布局,隐藏等改变而需要重新构建。这就称为回流。每个页面至少需要一次回流,就是在页面第一次加载的时候。
-
当render tree中的一些元素需要更新属性,而这些属性只是影响元素的外观,风格,而不会影响布局的,比如background-color。则就叫称为重绘。
注:回流必将引起重绘,而重绘不一定会引起回流。
我们前面知道了,回流这一阶段主要是计算节点的位置和几何信息,那么当页面布局和几何信息发生变化的时候,就需要回流。比如以下情况:
- 添加或删除可见的DOM元素
- 元素的位置发生变化
- 元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)
- 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代。
- 页面一开始渲染的时候(这肯定避免不了)
- 浏览器的窗口尺寸变化(因为回流是根据窗口的大小来计算元素的位置和大小的)
- 注意:JS 获取 Layout 属性值(如:offsetLeft、offsetWidth、offsetHeightscrollTop、getComputedStyle 等)也会引起回流。因为浏览器需要通过回流计算最新值
- 激活:hover伪类可能导致重排
性能优化:避免重绘与回流
1.由于display为none的元素在页面不需要渲染,渲染树构建不会包括这些节点;但visibility为hidden的元素会在渲染树中。因为display为none会脱离文档流,visibility为hidden虽然看不到,但类似与透明度为0,其实还在文档流中,还是有渲染的过程。
2.尽量避免使用表格布局,当我们不为表格td添加固定宽度时,一列的td的宽度会以最宽td的宽作为渲染标准,假设前几行td在渲染时都渲染好了,结果下面某行的一个td特别宽,table为了统一宽,前几行的td会回流重新计算宽度,这是个很耗时的事情。

3. 避免使用 css 表达式(expression),因为每次调用都会重新计算值(包括加载页面)
4. 尽量使用 css 属性简写,如:用 border 代替 border-width, border-style, border-color
5. 集中改变样式,往往通过改变class的⽅式来集中改变样式
6. 使⽤DocumentFragment,可以通过createDocumentFragment创建⼀个游离于DOM树之外的节点,然后在此节点上批量操作,最后插⼊ DOM树中,因此只触发⼀次重排
7. 提升为合成层
将元素提升为合成层有以下优点:
- 合成层的位图,会交由 GPU 合成,⽐ CPU 处理要快
- 当需要 repaint 时,只需要 repaint 本身,不会影响到其他的层
- 对于 transform 和 opacity 效果,不会触发 layout 和 paint
前端安全
web大前端开发中一些常见的安全性问题
XSS(Cross Site Script)
什么是XSS
跨站脚本攻击。它指的是恶意攻击者往Web页面里插入恶意html代码,当用户浏览该页之时,嵌入其中Web里面的html代码会被执行,从而达到恶意用户的特殊目的。
它与SQL注入攻击类似,SQL注入攻击中以SQL语句作为用户输入,从而达到查询/修改/删除数据的目的,而在xss攻击中,通过插入恶意脚本,实现对用户游览器的控制,获取用户的一些信息。
该网页把用户通过GET发送过来的表单数据,未经处理直接写入返回的html流,这就是XSS漏洞所在。
分类
XSS有三类:反射型XSS(非持久型)、存储型XSS(持久型)和DOM XSS。
1、反射型XSS
发出请求时,XSS代码出现在URL中,作为输入提交到服务器端,服务器端解析后响应,XSS代码随响应内容一起传回给浏览器,最后浏览器解析执行XSS代码。这个过程像一次反射,故叫反射型XSS。
2、存储型XSS
存储型XSS和反射型XSS的差别仅在于,提交的代码会存储在服务器端(数据库,内存,文件系统等),下次请求目标页面时不用再提交XSS代码。其他用户访问页面时服务器将带有XSS代码的页面返回,用户受到攻击
3、DOM XSS
DOM XSS和反射型XSS、存储型XSS的差别在于DOM XSS的代码并不需要服务器参与,触发XSS靠的是浏览器端的DOM解析,完全是客户端的事情。
触发方式为:http://www.a.com/xss/domxss.html#alert(1)
这个URL#后的内容是不会发送到服务器端的,仅仅在客户端被接收并解执行。直接输出html内容/直接修改DOM树/替换document URL/打开或修改新窗口
危害
- 挂马
- 盗取用户Cookie。
- DOS(拒绝服务)客户端浏览器。
- 钓鱼攻击,高级的钓鱼技巧。
- 删除目标文章、恶意篡改数据、嫁祸。
- 劫持用户Web行为,甚至进一步渗透内网。
- 蠕虫式的DDoS攻击。
- 蠕虫式挂马攻击、刷广告、刷浏量、破坏网上数据
预防
(1)对输入(用户输入/URL参数/POST请求参数等)进行非法字符过滤(对诸如,,,等。
1、 标签嵌入的跨域脚本。
2、 标签嵌入的CSS。由于CSS的松散的语法规则,CSS的跨域需要一个设置正确的 HTTP 头部 Content-Type 。不同浏览器有不同的限制。
3、 标签嵌入的图片。
4、 和 标签嵌入的多媒体资源。
5、、 和 标签嵌入的插件。
6、@font-face 引入的字体。一些浏览器允许跨域字体( cross-origin fonts),一些需要同源字体(same-origin fonts)。
7、 载入的任何资源。站点可以使用 X-Frame-Options 消息头来阻止这种形式的跨域交互
同站/同源
站(site)= eTLD+1
TLD表示顶级域名,例如.com、.org、.cn等等
TLD+1 表示顶级域名和它前面二级域名的组合,例如网址是:
https://www.example.com:443/foo
那么:
TLD是.comTLD+1是example.com
但是,这种表示方式是有缺陷的,例如对于下面的网址:
https://www.example.com.cn
如果按照上面的规则,它的 TLD+1 就是 com.cn,并不能表示这个站点,真正能表示这个站点的应该是 example.com.cn 才对,所以衍生出 eTLD 的概念,即有效顶级域名:
eTLD:com.cn
eTLD+1:example.com.cn
eTLD 由 Mozilla 维护在公共后缀列表(Public Suffix List)中,而「站」的定义就是这里的 eTLD+1。

响应头字段
Access-Control-Allow-Credentials:
这里的Credentials(凭证)其意包括:Cookie ,授权标头或 TLS 客户端证书,默认CORS请求是不带Cookies的,这与JSONP不同,JSONP每次请求都携带Cookies的,当然跨域允许带Cookies会导致CSRF漏洞。如果非要跨域传递Cookies,web端需要给ajax设置withCredentials为true,同时,服务器也必须使用Access-Control-Allow-Credentials头响应。此响应头true意味着服务器允许cookies(或其他用户凭据)包含在跨域请求中。另外,简单的GET请求是不预检的,即使请求的时候设置widthCrenditials为true,如果响应头不带Access-Control-Allow-Credentials,则会导致整个响应资源被浏览器忽略。Access-Control-Allow-HeadersAccess-Control-Allow-MethodsAccess-Control-Allow-OriginAccess-Control-Expose-Headers:
在CORS中,默认的,只允许客户端读取下面六个响应头(在axios响应对象的headers里能看到):
Cache-Control
Content-Language
Content-Type
Expires
Last-Modified
Pragma
如果这六个以外的响应头要是想让客户端读取到,就需要设置Access-Control-Expose-Headers这个为响应头名了,比如Access-Control-Expose-Headers: Token
6. Access-Control-Max-Age:设置预检请求的有效时长,就是服务器允许的请求方法和请求头做个缓存。
解决方案
CORS
CORS:全称"跨域资源共享"(Cross-origin resource sharing)。
CORS分为简单请求和 **非简单请求(需预检请求)**两类。
对于简单请求,浏览器会直接发送CORS请求,具体说来就是在header中加入origin请求头字段。同样,在响应头中,返回服务器设置的相关CORS头部字段,Access-Control-Allow-Origin字段为允许跨域请求的源。请求时浏览器在请求头的Origin中说明请求的源,服务器收到后发现允许该源跨域请求,则会成功返回。
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE';
add_header 'Access-Control-Allow-Headers' 'Content-Type';
CORS请求默认不发送Cookie和HTTP认证信息。但是如果要把Cookie发到服务器,要服务器同意,指定Access-Control-Allow-Credentials字段。
add_header 'Access-Control-Allow-Credentials' 'true';
需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。
当发生符合**非简单请求(预检请求)**的条件时,浏览器会自动先发送一个options请求,如果发现服务器支持该请求,则会将真正的请求发送到后端,反之,如果浏览器发现服务端并不支持该请求,则会在控制台抛出错误。
如果非简单请求(预检请求)发送成功,则会在头部多返回以下字段:
Access-Control-Allow-Origin: http://localhost:3001 //该字段表明可供那个源跨域
Access-Control-Allow-Methods: GET, POST, PUT // 该字段表明服务端支持的请求方法
Access-Control-Allow-Headers: X-Custom-Header // 实际请求将携带的自定义请求首部字段
优点:CORS支持所有类型的HTTP请求,功能完善;可以通过onerror事件监听错误,并且浏览器控制台会看到报错信息,利于排查。
缺点:目前主流浏览器支持CORS,但IE10以下不支持CORS;
详见:
https://blog.csdn.net/badmoonc/article/details/82706246
JSONP
为什么jsonp动态创建script标签可以解决跨域:
利用 标签没有跨域限制的漏洞,网页可以得到从其他来源动态产生的 JSON 数据。JSONP请求一定需要对方的服务器做支持才可以。
优点:简单,兼容性好,可用于解决主流浏览器的跨域数据访问的问题。
缺点:仅支持get方法,具有局限性;不安全,可能会遭受XSS攻击;错误处理机制并不完善
JSONP的实现流程
-
在客户端声明一个回调函数,其函数名(如show)当做参数值,要传递给跨域请求数据的服务器,函数形参为要获取目标数据(服务器返回的data)。
-
创建一个
标签,把那个跨域的API数据接口地址,赋值给script的src,还要在这个地址中向服务器传递该函数名(可以通过问号传参:?callback=show)。 -
服务器接收到请求后,需要进行特殊的处理:把传递进来的函数名和它需要给你的数据拼接成一个字符串,例如:传递进去的函数名是show,它准备好的数据是 show(‘helloworld’)。
-
最后服务器把准备的数据通过HTTP协议返回给客户端,客户端再调用执行之前声明的回调函数(show),对返回的数据进行操作。
详见:
https://blog.csdn.net/badmoonc/article/details/82289252
Nginx反向代理
反向代理:是指以代理服务器来接受Internet上的连接请求,然后将请求转发给内部网络上的服务器;并将从服务器上得到的结果返回给Internet上请求连接的客户端,此时代理服务器对外就表现为一个服务器。支持所有浏览器,支持session,不需要修改任何代码,并且不会影响服务器性能。
例如:
- 前端server域名为:http://xx_domain
- 后端server域名为:https://github.com
现在http://xx_domain对https://github.com发起请求一定会出现跨域。
不过只需要启动一个nginx服务器,将server_name设置为xx_domain,然后设置相应的location以拦截前端需要跨域的请求,最后将请求代理回github.com。如下面的配置:
server {listen 80;server_name xx_domain;location / {proxy_pass github.com;}
}
其它跨域⽅案
- HTML5 XMLHttpRequest 有⼀个API,
postMessage()⽅法允许来⾃不同源的脚本采⽤异步⽅式进⾏有限的通信, 可以实现跨⽂本档、多窗⼝、跨域消息传递。
var popup = window.open('http://bbb.com', 'title');
popup.postMessage('Hello World!', 'http://bbb.com');
postMessage方法的第一个参数是具体的信息内容,第二个参数是接收消息的窗口的源(origin),即"协议 + 域名 + 端口"。也可以设为*,表示不限制域名,向所有窗口发送。
父窗口和子窗口都可以通过message事件,监听对方的消息。
window.addEventListener('message', function(e) {console.log(e.data);
},false);
message事件的事件对象event,提供以下三个属性。
- event.source:发送消息的窗口
- event.origin: 消息发向的网址
- event.data: 消息内容
- WebSocket 是⼀种双向通信协议,在建⽴连接之后,WebSocket 的 server 与 client 都能主动向对⽅发送或接收数 据,连接建⽴好了之后 client 与 server 之间的双向通信就与 HTTP ⽆关了,因此可以跨域。
window.name + iframe:window.name属性值在不同的⻚⾯(甚⾄不同域名)加载后依旧存在,并且可以⽀持⾮常 ⻓的 name 值,我们可以利⽤这个特点进⾏跨域。 - location.hash + iframe:a.html欲与c.html跨域相互通信,通过中间⻚b.html来实现。 三个⻚⾯,不同域之间利⽤ iframe的location.hash传值,相同域之间直接js访问来通信。
- document.domain + iframe: 该⽅式只能⽤于⼆级域名相同的情况下,⽐如 a.test.com 和 b.test.com 适⽤于该⽅ 式,我们只需要给⻚⾯添加
- document.domain =‘test.com’ 表示⼆级域名都相同就可以实现跨域,两个⻚⾯都通过js 强制设置document.domain为基础主域,就实现了同域。
跨域的登录态是怎么保持的
目前,状态判断的一种很流行方式是cookie与session结合实现的。当你输入帐号密码后,向服务器发送请求,服务器判断你登录成功后,会将你的登录状态记录到session中,并且给浏览器返回一个session_id,浏览器在cookie中保存这个session_id,在下一次向服务器请求数据的时候,会通过cookie将session_id传给服务器,服务器通过session_id找到对应的session就可以判断你登录的状态了。
- 前端要将withCredentials设为true
- 后端要将Access-Control-Allow-Credentials 设为true,因此Access-Control-Allow-Origin就不能设为*了,需要改成具体的域了,这样就可以多次请求取到的sessionid就一致了。
https://blog.csdn.net/merit_pig/article/details/106446774
扫码登录如何实现
扫码登陆要求已经用户在手机端登录
- 用户在浏览器点击扫码登录,浏览器向服务端发送一个请求,服务端生成一个唯一 id,并将这个 id 写入二维码并且返回这个二维码给浏览器获(获取唯一的id, 以及包含id信息的二维码)
- 浏览器收到之后,在页面上显示这个二维码,开始用这个 id 轮询后台提供的一个接口,判断用户是否扫码并确认登陆了(浏览器轮询服务器,获取扫码状态),然后根据服务器返回的扫码状态,进行相应的操作
- 408 扫码超时:如果手机没有扫码或没有授权登录,服务器会阻塞约25s,然后返回状态码 408 -> 前端继续轮询
- 400 二维码失效:大约5分钟的时间内不扫码,二维码失效
- 201 已扫码:如果手机已经扫码,服务器立即返回状态码和用户的基本信息
- 200 已授权:如果手机点击了确认登录,服务器返回200及token -> 前端停止轮询, 获取到token,重定向到目标页
- 用户用手机扫描二维码,得到这个 id,再将手机的身份信息与二维码id一起发送给服务端
- 服务端将身份信息与二维码id进行绑定,生成一个临时token(只能使用一次)返回给手机端
- 手机接收到临时token后弹出确认登陆界面,并确认登陆,然后携带临时token调用服务端接口,告诉服务端已经确认登陆了
- 服务器拿到用户信息和 id 之后,写入数据库,并生成token返回给浏览器
- 这时候浏览器的服务器轮询就会得到结果,说明用户已经确认登陆,并且得到服务器返回的 token 和用户信息。
前端发送请求的方法总结
- 原生的ajax
- jquery 发送ajax请求
- axios
- Vue-resource:
this.$http - fetch
- 表单
验证码实现
1、后端随机生成一段字符串/数字;
2、将随机字符串存到缓存(或session),同时将字符串生成一张图片/或第三方工具使用短信接口发送给用户;
3、然后将图片的路径放回到前端。前端用img标签展示图片;
4、前端输入的验证码传到后端;
5、后端比对就OK了。
浏览器大量http请求怎么优化
Stalled(阻塞):
浏览器对同一个主机域名的并发连接数有限制,因此如果当前的连接数已经超过上限,那么其余请求就会被阻塞,等待新的可用连接;此外脚本也会阻塞其他组件的下载;
优化措施:
1、将资源合理分布到多台主机上,可以提高并发数,但是增加并行下载数量也会增大开销,这取决于带宽和CPU速度,过多的并行下载会降低性能;
2、脚本置于页面底部;
DNS Lookup(域名解析):
请求某域名下的资源,浏览器需要先通过DNS解析器得到该域名服务器的IP地址。在DNS查找完成之前,浏览器不能从主机名那里下载到任何东西。
优化措施:
1、利用DNS缓存(设置TTL时间);
2、利用Connection:keep-alive特性建立持久连接,可以在当前连接上进行多个请求,无需再进行域名解析;
Request sent(发送请求):
发送HTTP请求的时间(从第一个bit到最后一个bit)
优化措施:
1、减少HTTP请求,可以使用CSS Sprites、内联图片、合并脚本和样式表等;
2、**浏览器缓存:**对不常变化的组件添加长久的Expires头(相当于设置久远的过期时间),在后续的页面浏览中可以避免不必要的HTTP请求;
Waiting(等待响应):
通常是耗费时间最长的。从发送请求到收到响应之间的空隙,会受到线路、服务器距离等因素的影响。
优化措施:
使用CDN(内容分发网络),将用户的访问指向距离最近的工作正常的缓存服务器上,由缓存服务器直接响应用户请求,提高响应速度;
Content Download(下载):
下载HTTP响应的时间(包含头部和响应体)
优化措施:
1、浏览器缓存:通过条件Get请求,对比If-Modified-Since和Last-Modified时间,确定是否使用缓存中的组件,服务器会返回“304 Not Modified”状态码,减小响应的大小;
2、移除重复脚本,精简和压缩代码,如借助自动化构建工具webpack、grunt、gulp等;
3、压缩响应内容,服务器端启用gzip压缩,可以减少下载时间;
CSS/JS文件加载是否阻塞DOM解析/渲染
- CSS:不会阻塞DOM解析,会阻塞DOM树的渲染,会阻塞后面js语句的执行
- 加载css的时候,可能会修改下面DOM节点的样式,如果css加载不阻塞DOM树渲染的话,那么当css加载完之后,DOM树可能又得重新重绘或者回流了,这就造成了一些没有必要的损耗。所以干脆就先把DOM树的结构先解析完,把可以做的工作做完,然后等你css加载完之后,在根据最终的样式来渲染DOM树,这种做法性能方面确实会比较好一点。
- 为了避免让用户看到长时间的白屏时间,我们应该尽可能的提高css加载速度,比如可以使用以下几种方法:
- 使用CDN(因为CDN会根据你的网络状况,替你挑选最近的一个具有缓存内容的节点为你提供资源,因此可以减少加载时间)
- 对css进行压缩(可以用很多打包工具,比如webpack,gulp等,也可以通过开启gzip压缩)
- 合理的使用缓存(设置cache-control,expires,以及E-tag都是不错的,不过要注意一个问题,就是文件更新后,你要避免缓存而带来的影响。其中一个解决防范是在文件名字后面加一个版本号)
- 减少http请求数,将多个css文件合并,或者是干脆直接写成内联样式(内联样式的一个缺点就是不能缓存)
- JS:会阻塞DOM解析,没有async/defer属性的script标签会阻塞DOM渲染
- 浏览器并不知道脚本的内容是什么,如果先行解析下面的DOM,万一脚本内全删了后面的DOM,浏览器就白干活了。更别谈丧心病狂的document.write。浏览器无法预估里面的内容,那就干脆全部停住,等脚本执行完再干活就好了。
- 浏览器遇到
且没有defer或async属性的标签时,会触发页面渲染,因而如果前面CSS资源尚未加载完毕时,浏览器会等待它加载完毕在执行脚本。
- 总结
最好放底部,最好放头部,如果头部同时有与的情况下,最好将放在上面
xhr与fetch
fetch 同 XMLHttpRequest 非常类似,都是用来做网络请求。但是同复杂的XMLHttpRequest的API相比,fetch使用了Promise,这让它使用起来更加简洁,从而避免陷入”回调地狱”。
XMLHttpRequest

- 创建XHRHttpResquest对象
var xmlHttp;
if (typeof XMLHttpRequest != "undefined") {// 高版本浏览器都支持xmlHttp = new XMLHttpRequest();
} else if (window.ActiveXObject) {// 其他低版本浏览器var aVersions = ["Msxml2.XMLHttp.5.0", "Msxml2.XMLHttp.4.0", "Msxml2.XMLHttp.3.0", "Msxml2.XMLHttp", "Microsoft.XMLHttp"];for (var i = 0; i < aVersions.length; i++) {try {xmlHttp = new ActiveXObject(aVersions[i]);break;} catch (e) {}}
}
大体流程
xhr.timeout = xxx;// 初始化一个请求xhr.open(method, url, async);// setRequestHeader 必须在 open() 之后、send() 之前调用xhr.setRequestHeader(header, value)//设置响应返回的数据格式xhr.responseType = "text";// 监听状态xhr.onreadystatechange = function() {if (xhr.status === 2) {// 返回所有的响应头console.log(xhr.getAllResponseHeaders())// 返回特定响应头 // ⚠️注意:规定客户端无法获取 response 中的 Set-Cookie、Set-Cookie2这2个字段,无论是同域还是跨域请求。//规定对于跨域请求,客户端允许获取的response header字段只限于 simple response headerxhr.getResponseHeader("Content-Type");}if (xhr.status === 4 || xhr.readyState ===200){// 返回的纯文本的值console.log(xhr.responseText)// HTML 节点或解析后的 XML 节点,也可能是在没有收到任何数据或数据类型错误的情况下返回的 nullconsole.log(xhr.responseXML);}}// 超时处理xhr.ontimeout = function() { xxx }// 错误处理xhr.onerror = function() {xxx}// 发送请求xhr.send(data) // POST数据放在data中,GET会忽略这个参数// xhr.send(data)中data参数的数据类型会影响请求头部content-type的默认值://如果data是 Document 类型,同时也是HTML Document类型,则content-type默认值为text/html;charset=UTF-8;否则为application/xml;charset=UTF-8;//如果data是 DOMString 类型,content-type默认值为text/plain;charset=UTF-8;//如果data是 FormData 类型,content-type默认值为multipart/form-data; boundary=[xxx]//如果data是其他类型,则不会设置content-type的默认值//当然这些只是content-type的默认值,但如果用xhr.setRequestHeader()手动设置了中content-type的值,以上默认值就会被覆盖。
获取上传和下载进度
xhr.onprogress = progessFn;
xhr.upload.onprogress = progessFnfunction progessFn(event) {if (event.lengthComputable) {console.log(event.loaded / event.total)}
}
Fetch 优点
MDN - fetch()
- fetch的语法简洁,更语义化
- 基于promise,支持async/await
- 同构方便,使用isomorphic-fetch
Fetch的缺点
- fetch只对网络错误报错,http状态码错误不报错
- fetch不支持abort,无法终止
- fetch不支持超时控制,使用setTimeout和Promise.reject实现的超时控制不能阻止请求过程继续在后台运行,造成了流量的浪费
- fetch没有原生检测请求进度的方式,XHR可以
- 默认情况下fetch不发送cookie,除非手动配置
原生JS操作DOM方法
DOM 创建
var el1 = document.createElement('div');
var node = document.createTextNode('hello world!');
DOM 查询
// 返回当前文档中第一个类名为 "myclass" 的元素
var el = document.querySelector(".myclass");// 返回一个文档中所有的class为"note"或者 "alert"的div元素
var els = document.querySelectorAll("div.note, div.alert");// 获取元素
var el = document.getElementById('xxx');
var els = document.getElementsByClassName('highlight');
var els = document.getElementsByTagName('td');// 获取父元素、父节点
var parent = ele.parentElement;
var parent = ele.parentNode;// 获取子节点,子节点可以是任何一种节点,可以通过nodeType来判断
var nodes = ele.children; // 查询子元素
var els = ele.getElementsByTagName('td');
var els = ele.getElementsByClassName('highlight');// 当前元素的第一个/最后一个子元素节点
var el = ele.firstChild;
var el = ele.firstElementChild;var el = ele.lastChild;
var el = ele.lastElementChild;// 下一个/上一个兄弟元素节点
var el = ele.nextSibling;
var el = ele.nextElementSibling;var el = ele.previousSibling;
var el = ele.previousElementSibling;
DOM 更改
// 添加、删除子元素
ele.appendChild(el);
ele.removeChild(el);// 替换子元素
ele.replaceChild(el1, el2);// 插入子元素
parentElement.insertBefore(newElement, referenceElement);
属性操作
// 获取一个{name, value}的数组
var attrs = el.attributes;// 获取、设置属性
var c = el.getAttribute('class');
el.setAttribute('class', 'highlight');// 判断、移除属性
el.hasAttribute('class');
el.removeAttribute('class');// 是否有属性设置
el.hasAttributes();
前端缓存

HTTP缓存机制


强缓存
在浏览器第一次发出请求之后,需要再次发送请求的时候,浏览器首先获取该资源缓存的 header 信息,然后根据 Cache-Control 和 Expires 字段判断缓存是否过期。如果没有过期,直接使用浏览器缓存,并不会与服务器通信。该过程为判断是否使用强缓存,即本地缓存。
Cache-Control字段是 HTTP1.1 规范,一般利用该字段的 max-age 属性来判断,这个值是一个相对时间,单位为 s,代表资源的有效期。例如:
Cache-Control:max-age=3600
除此之外还有几个常用的值:
- no-cache:表示不使用强缓存,需要使用协商缓存
- no-store:禁止浏览器缓存数据,每次请求下载完整的资源
- public:可以被所有用户缓存,包括终端用户和中间代理服务器
- private:只能被终端用户的浏览器缓存
Expires字段是 HTTP1.0 规范,他是一个绝对时间的 GMT 格式的时间字符串。例如:
expires:Mar, 06 Apr 2020 10:57:09 GMT
这个时间代表资源的失效时间,只要发送请求的时间在这之前,都会使用强缓存。
为什么出现Cache-Control
由于失效时间是一个绝对时间(本地时间),因此当服务器时间与客户端时间偏差较大时,就会导致缓存混乱。
值得注意的是expires时间可能存在客户端时间跟服务端时间不一致的问题。所以,建议expires结合Cache-Control一起使用
而Cache-Control的max-age是一个相对时间,消除了服务器时间和客户端时间偏差带来的问题。
协商缓存
如果缓存过期,浏览器会向服务器发送请求,即使用协商缓存。本次请求会带着第一次请求返回的有关缓存的 header 字段信息,比如以下两个字段:
Etag/If-None-Match
ETag是响应头,If-None-Match是请求头。
Last-Modified / If-Modified-Since的主要缺点就是它只能精确到秒的级别,一旦在一秒的时间里出现了多次修改,那么Last-Modified / If-Modified-Since是无法体现的。
判断响应头中是否存在 Etag 字段,如果存在,浏览器则发送一个带有 If-None-Match 字段的请求头的请求,该字段的值为 Etag 值。服务器通过对比客户端发过来的Etag值是否与服务器相同。如果相同,说明缓存命中,服务器返回 304 状态码,并将 If-None-Match 设为 false, 客户端继续使用本地缓存。如果不相同,说明缓存未命中,服务器返回 200 状态码,并将 If-None-Match 设为 true,并且返回请求的数据。
Last-Modified/If-Modified-Since
Last-Modified是响应头,If-Modified-Since是请求头。
除了 Etag 字段之外,客户端还会通过服务器返回的 Last-Modified 字段判断是否继续使用缓存,该字段为服务器返回的资源的最后修改时间,为UMT时间。浏览器发送一个带有 If-Modified-Since 字段的请求头的请求给服务器,该字段的值为 Last-Modified 值。服务器收到之后,通过这个时间判断,在该时间之后,资源有无修改,如果未修改,缓存命中,返回 304 状态码;如果未命中,返回 200 状态码,并返回最新的内容。
Cache-Control 与 Expires 的优先级:
两者可以在服务端配置同时使用,Cache-Control 的优先级高于 Expires。
Last-Modified/If-Modified-Since 已经可以判断缓存是否失效了,为什么出现 Etag/If-None-Match?
Etag/If-None-Match 是实体标签,是一个资源的唯一标识符,资源的变化都会导致 ETag 的变化。出现 Etag 的主要原因是解决 Last-Modified 比较难解决的问题:
- 一些文件也许会周期性的修改,但是他的内容并不发生改变,这个时候我们并不希望客户端认为这个文件修改了
- 某些文件在秒以下的时间内进行修改了,If-Modified-Since无法判断。UNIX时间只能精确到秒
Last-Modified 和 Etag 可以一起使用, Etag 的优先级更高。
刷新页面的问题:
F5刷新:不使用强缓存,使用协商缓存
ctrl+F5:二者都不使用

Etag生成
总结:nginx 中 etag 由响应头的 Last-Modified 与 Content-Length 表示为十六进制组合而成。
离线缓存
概念和优势
离线缓存是Html5新特性之一,简单理解就是第一次加载后将数据缓存,在没有清除缓存前提下,下一次没有网络也可以加载,用在静态数据的网页或游戏比较好用。
(1)在没有网络的时候可以访问到缓存的对应的站点页面,包括html,js,css,img等等文件
(2)在有网络的时候,浏览器会优先使用已离线存储的文件,返回一个200(from cache)头。这跟HTTP的缓存实用策略是不同的
(3)资源的缓存可以带来更好的用户体验,当用户使用自己的流量上网时,本地缓存不仅可以提高用户访问速度,而且大大节约用户的使用流量。
离线缓存的优缺点
优点
- 减少服务器的负载,提高资源加载速度
- 离线浏览,用户可以在应用离线时使用
缺点
- 更新版本后,必须刷新一次才会启动新版本。
- 进入离线存储的页面,如果不更新版本,是会将其当成静态页面不请求。
- 无法进行灰度发布等策略
离线缓存与传统浏览器缓存区别:
- 离线缓存是针对整个应用,浏览器缓存是单个文件
- 离线缓存断网了还是可以打开页面,浏览器缓存不行
- 离线缓存可以主动通知浏览器更新资源
如何使用
(1)在需要缓存的和html文件的根节点(html)添加manifest属性,属性值是当前目录下的一个.appcache文件/或.manifest

2)编写test.mainfest文件。
CACHE MANIFEST//必须以这个开头
version 1.0 //最好定义版本,更新的时候只需修改版本号
CACHE:
a.css
NETWORK:
b.css
FALLBACK:
c.ss a.css
说明:CACHE下面的都是缓存的文件,NETWORK表示每次都从网络请求,FALLBACK:指定的文件若是找不到,会被重定向到新的地址。注意,第一行必须是”CACHE MANIFEST”文字,以把本文件的作用告知浏览器,即对本地缓存中的资源文件进行具体设置。


说明:浏览器从APPcache中获取资源及数据,APPcache会访问服务器查看是否有资源更新,没有则直接返回,有的话先更新下载服务端资源并缓存本地,之后更新浏览器显示最新数据。
HTML5存储类型
web storage
- localStorage:适用于长期存储数据,以键值对(Key-Value)的方式存储,永久存储,永不失效,除非手动删除。浏览器关闭后数据不丢失;每个域名5M
- sessionStorage:存储的数据在浏览器关闭后自动删除。⽽且与cookie、localStorage不同,他不能在所有同源窗⼝中共享,是会话级别的储存⽅式
离线缓存(application cache)
见上方离线缓存
Web SQL
关系数据库,通过SQL语句访问。
Web SQL 数据库 API 并不是 HTML5 规范的一部分,但是它是一个独立的规范,引入了一组使用 SQL 操作客户端数据库的 APIs。web sql类似于SQLite,是真正意义上的关系型数据库,⽤sql进⾏操作,当我们⽤JavaScript时要进⾏转换,较为繁琐。
核心方法:
① openDatabase:这个方法使用现有的数据库或者新建的数据库创建一个数据库对象。
② transaction:这个方法让我们能够控制一个事务,以及基于这种情况执行提交或者回滚。
③ executeSql:这个方法用于执行实际的 SQL 查询。
IndexedDB
索引数据库 (IndexedDB) API(作为 HTML5 的一部分)对创建具有丰富本地存储数据的数据密集型的离线 HTML5 Web 应用程序很有用。同时它还有助于本地缓存数据,使传统在线 Web 应用程序(比如移动 Web 应用程序)能够更快地运行和响应。
是被正式纳⼊HTML5标准的数据库储存⽅案,它是NoSQL数据库,⽤键值对进⾏储存,可以进⾏快速读取操作,⾮常适合web场景,同时⽤JavaScript进⾏操作会⾮常⽅便。
打开数据库:
window.indexedDB.open('testDB')
关闭与删除:
function closeDB(db){db.close();
}
function deleteDB(name){indexedDB.deleteDatabase(name);
}
数据存储:
indexedDB中没有表的概念,而是objectStore,**一个数据库中可以包含多个objectStore,objectStore是一个灵活的数据结构,可以存放多种类型数据。**也就是说一个objectStore相当于一张表,里面存储的每条数据和一个键相关联。
我们可以使用每条记录中的某个指定字段作为键值(keyPath),也可以使用自动生成的递增数字作为键值(keyGenerator),也可以不指定。选择键的类型不同,objectStore可以存储的数据结构也有差异。
Service Worker
Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用 Service Worker的话,传输协议必须为 HTTPS。因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全。
Service Worker 实现缓存功能一般分为三个步骤:
- 首先需要先注册 Service Worker
- 然后监听到 install 事件以后就可以缓存需要的文件
- 那么在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据。
memory cache 和 disk cache
MemoryCache顾名思义,就是将资源缓存到内存中,等待下次访问时不需要重新下载资源,而直接从内存中获取。
目前Webkit资源分成两类,一类是主资源,比如HTML页面,或者下载项,一类是派生资源,比如HTML页面中内嵌的图片或者脚本链接,分别对应代码中两个类:MainResourceLoader和SubresourceLoader。虽然Webkit支持memoryCache,但是也只是针对派生资源,它对应的类为CachedResource,用于保存原始数据(比如CSS,JS等),以及解码过的图片数据。
diskCache顾名思义,就是将资源缓存到磁盘中,等待下次访问时不需要重新下载资源,而直接从磁盘中获取。它与memoryCache最大的区别在于,当退出进程时,内存中的数据会被清空,而磁盘的数据不会,所以,当下次再进入该进程时,该进程仍可以从diskCache中获得数据,而memoryCache则不行。
diskCache与memoryCache相似之处就是也只能存储一些派生类资源文件。它的存储形式为一个index.dat文件,记录存储数据的url,然后再分别存储该url的response信息和content内容。Response信息最大作用就是用于判断服务器上该url的content内容是否被修改。
小结:一般图片会用disk cache(非脚本文件), js文件用memory cache(脚本文件)
常⻅的浏览器内核有哪些

前端如何实现即时通讯
前端如何实现即时通讯?
AJAX原理
Ajax相当于在用户和服务器之间加了—个中间层,使用户操作与服务器响应异步化。并不是所有的用户请求都提交给服务器,像—些数据验证和数据处理等都交给Ajax引擎自己来做, 只有确定需要从服务器读取新数据时再由Ajax引擎代为向服务器提交请求。
Ajax的原理简单来说就是:通过XmlHttpRequest对象来向服务器发送异步请求,从服务器获得数据,然后用javascript来操作DOM而更新页面。这其中最关键的一步就是从服务器获得请求数据。要清楚这个过程和原理,我们必须对 XMLHttpRequest有所了解。
XMLHttpRequest是ajax的核心机制,它是在IE5中首先引入的,是一种支持异步请求的技术。简单的说,也就是javascript可以及时向服务器提出请求和处理响应,而不阻塞用户。达到无刷新的效果。
第一步:创建XMLHttpRuquest对象;
第二步:注册回调方法
第三步:设置和服务器交互的相应参数
第四步:设置向服务器端发送的数据,启动和服务器端的交互
第五步:判断和服务器端的交互是否完成,还要判断服务器端是否返回正确的数据
window.onload DOMContentLoaded区别
DOM完整的解析过程:
- 解析HTML结构。
- 加载外部脚本和样式表文件。
- 解析并执行脚本代码。//js之类的
- DOM树构建完成。//DOMContentLoaded
- 加载图片等外部文件。
- 页面加载完毕。//load
在第4步的时候DOMContentLoaded事件会被触发。
在第6步的时候onload事件会被触发。
1、当 onload事件触发时,页面上所有的DOM,样式表,脚本,图片,flash都已经加载完成了。
2、当 DOMContentLoaded 事件触发时,仅当DOM加载完成,不包括样式表,图片,flash。

- 为什么要区分?
开发中我们经常需要给一些元素的事件绑定处理函数。但问题是,如果那个元素还没有加载到页面上,但是绑定事件已经执行完了,是没有效果的。这两个事件大致就是用来避免这样一种情况,将绑定的函数放在这两个事件的回调中,保证能在页面的某些元素加载完毕之后再绑定事件的函数。
当然DOMContentLoaded机制更加合理,因为我们可以容忍图片,flash延迟加载,却不可以容忍看见内容后页面不可交互。
单点登陆原理
在同域下的客户端/服务端认证系统中,通过客户端携带凭证,维持一段时间内的登录状态。
但当我们业务线越来越多,就会有更多业务系统分散到不同域名下,就需要「一次登录,全线通用」的能力,叫做「单点登录」。

- 用户访问系统1的受保护资源,系统1发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数
- sso认证中心发现用户未登录,将用户引导至登录页面
- 用户输入用户名密码提交登录申请
- sso认证中心校验用户信息,创建用户与sso认证中心之间的会话,称为全局会话,同时创建授权令牌
- sso认证中心带着令牌跳转会最初的请求地址(系统1)
- 系统1拿到令牌,去sso认证中心校验令牌是否有效
- sso认证中心校验令牌,返回有效,注册系统1
- 系统1使用该令牌创建与用户的会话,称为局部会话,返回受保护资源
- 用户访问系统2的受保护资源
- 系统2发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数
- sso认证中心发现用户已登录,跳转回系统2的地址,并附上令牌
- 系统2拿到令牌,去sso认证中心校验令牌是否有效
- sso认证中心校验令牌,返回有效,注册系统2
- 系统2使用该令牌创建与用户的局部会话,返回受保护资源
用户登录成功之后,会与sso认证中心及各个子系统建立会话,用户与sso认证中心建立的会话称为全局会话,用户与各个子系统建立的会话称为局部会话,局部会话建立之后,用户访问子系统受保护资源将不再通过sso认证中心,全局会话与局部会话有如下约束关系
- 局部会话存在,全局会话一定存在
- 全局会话存在,局部会话不一定存在
- 全局会话销毁,局部会话必须销毁
考虑浏览器
对浏览器来说,SSO 域下返回的数据要怎么存,才能在访问 A 的时候带上?浏览器对跨域有严格限制,cookie、localStorage 等方式都是有域限制的。
这就需要也只能由 A 提供 A 域下存储凭证的能力。一般我们是这么做的:

- 在 SSO 域下,SSO 不是通过接口把 ticket 直接返回,而是通过一个带 code 的 URL 重定向到系统 A 的接口上,这个接口通常在 A 向 SSO 注册时约定
- 浏览器被重定向到 A 域下,带着 code 访问了 A 的 callback 接口,callback 接口通过 code 换取 ticket
- 这个 code 不同于 ticket,code 是一次性的,暴露在 URL 中,只为了传一下换 ticket,换完就失效
- callback 接口拿到 ticket 后,在自己的域下 set cookie 成功
- 在后续请求中,只需要把 cookie 中的 ticket 解析出来,去 SSO 验证就好
访问 B 系统也是一样
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

