fsme

fsme 查看完整档案

北京编辑  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

fsme 赞了文章 · 1月25日

Dom 节点和 元素 有啥区别?好家伙,我弄懂了!

作者:Shadeed
译者:前端小智
来源:dmitripavlutin
点赞再看,微信搜索大迁世界,B站关注前端小智这个没有大厂背景,但有着一股向上积极心态人。本文 GitHubhttps://github.com/qq44924588... 上已经收录,文章的已分类,也整理了很多我的文档,和教程资料。

最近开源了一个 Vue 组件,还不够完善,欢迎大家来一起完善它,也希望大家能给个 star 支持一下,谢谢各位了。

github 地址:https://github.com/qq44924588...

文档对象模型(DOM)是一个将HTML或XML文档视为树形结构的接口,其中每个节点都是文档的一个对象。DOM还提供了一组方法来查询树、改变结构、样式。

DOM 还使用术语元素(Element)它与节点非常相似。那么,DOM节点和元素之间有什么区别呢?

1. DOM节点

理解节点和元素之间区别的关键是理解节点是什么。

更高的角度来看,DOM文档由节点层次结构组成。 每个节点可以具有父级和/或子级。

看看下面的HTML文档:

<!DOCTYPE html>
<html>
  <head>
    <title>My Page</title>
  </head>
  <body>
    <!-- Page Body -->
    <h2>My Page</h2>
    <p id="content">Thank you for visiting my web page!</p>
  </body>
</html>

该文档包含以下节点层次结构:

clipboard.png

<html>是文档树中的一个节点。它有2个子节点:<head><body>

<body>子有3个子节点的节点:注释节点 <!-- Page Body -->,标题<h2>,段落<p><body>节点的父节点是<html>节点。

HTML文档中的标记代表一个节点,有趣的是普通文本也是一个节点。段落节点<p>有1个子节点:文本节点“Thank you for visiting my web page!”

1.2节点类型

我们要如何区分这些不同类型的节点? 答案在于DOM Node接口,尤其是Node.nodeType属性。

Node.nodeType可以具有代表节点类型的以下值之一:

  • Node.ELEMENT_NODE
  • Node.ATTRIBUTE_NODE
  • Node.TEXT_NODE
  • Node.CDATA_SECTION_NODE
  • Node.PROCESSING_INSTRUCTION_NODE
  • Node.COMMENT_NODE
  • Node.DOCUMENT_NODE
  • Node.DOCUMENT_TYPE_NODE
  • Node.DOCUMENT_FRAGMENT_NODE
  • Node.NOTATION_NODE

常量有意义地指示节点类型:例如Node.ELEMENT_NODE代表元素节点,Node.TEXT_NODE代表文本节点,Node.DOCUMENT_NODE文档节点,依此类推。

例如,让我们选择段落节点,然后查看其nodeType属性:

const paragraph = document.querySelector('p');

paragraph.nodeType === Node.ELEMENT_NODE; // => true

代表整个节点文档树的节点类型为Node.DOCUMENT_NODE

document.nodeType === Node.DOCUMENT_NODE; // => true

2. DOM元素

掌握了DOM节点的知识之后,现在该区分DOM节点和元素了。

如果你了解节点术语,那么答案是显而易见的:元素是特定类型的节点 element (Node.ELEMENT_NODE),以及文档、注释、文本等类型。

简而言之,元素是使用HTML文档中的标记编写的节点。 <html><head><title><body><h2><p>都是元素,因为它们由标签表示。

文档类型,注释,文本节点不是元素,因为它们没有使用标签编写:

Node是节点的构造函数,HTMLElement 是 JS DOM 中元素的构造函数。段落既是节点又是元素,它同时是NodeHTMLElement的实例

const paragraph = document.querySelector('p');

paragraph instanceof Node;        // => true
paragraph instanceof HTMLElement; // => true

简单地说,元素是节点的子类型,就像猫是动物的子类型一样。

3. DOM属性:节点和元素

除了区分节点和元素之外,还需要区分只包含节点或只包含元素的DOM属性。

节点类型的以下属性评估为一个节点或节点集合(NodeList):

node.parentNode; // Node or null

node.firstChild; // Node or null
node.lastChild;  // Node or null

node.childNodes; // NodeList

但是,以下属性是元素或元素集合(HTMLCollection):

node.parentElement; // HTMLElement or null

node.children;      // HTMLCollection

由于node.childNodes和node.children都返回子级列表,因此为什么要同时具有这两个属性? 好问题!

考虑以下包含某些文本的段落元素:

<p>
  <b>Thank you</b> for visiting my web page!
</p>

打开演示,然后查看parapgraph节点的childNodeschildren属性:

const paragraph = document.querySelector('p');

paragraph.childNodes; // NodeList:       [HTMLElement, Text]
paragraph.children;   // HTMLCollection: [HTMLElement]

paragraph.childNodes集合包含2个节点: <b>Thank you</b>,,以及for visiting my web page!文本节点!

但是,paragraph.children集合仅包含1个项目:<b>Thank you</b>

由于paragraph.children仅包含元素,因此此处未包含文本节点,因为其类型是文本(Node.TEXT_NODE),而不是元素(Node.ELEMENT_NODE)。

同时拥有node.childNodesnode.children,我们可以选择要访问的子级集合:所有子级节点或仅子级是元素。

4.总结

DOM文档是节点的分层集合,每个节点可以具有父级和/或子级。如果了解节点是什么,那么了解DOM节点和元素之间的区别就很容易。

节点有类型,元素类型就是其中之一,元素由HTML文档中的标记表示。

完~ 我是小智,我要去刷碗了,我们下期再见!


代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

原文:https://dmitripautin.com/dom-...

交流

文章每周持续更新,可以微信搜索「 大迁世界 」第一时间阅读和催更(比博客早一到两篇哟),本文 GitHub https://github.com/qq449245884/xiaozhi 已经收录,整理了很多我的文档,欢迎Star和完善,大家面试可以参照考点复习,另外关注公众号,后台回复福利,即可看到福利,你懂的。

查看原文

赞 5 收藏 4 评论 0

fsme 赞了文章 · 1月12日

2020年终总结——前端入坑四年,今年实惨

写在前面

2020最后一天,我在KFC写总结。前几天就想写了,嗯,我是拖延症患者。😔 2020年,换工作、被裁员,体检一堆问题... 回顾我31年的人生,没有重点小学、没有重点初高中、专科学历、没有获奖经历、没有特殊技能、学习能力差、英语不好、拖延.... 加上现在算大龄程序员了吧,似乎我的人生拿了一把烂牌,可是我还是想赢啊。

我怎么走上了前端这条路?

11年毕业后,在工厂里打工,做技术员,跟IT无关。最好的4年光阴,感觉错付了,但是不后悔,每个人都有不同的经历。然后14年的时候,觉得这么下去我能看见我退休甚至死亡的时刻。所以我觉得我应该学点什么?好让我的人生稍微有点不同。 于是我买了大学接触过的单片机,想做硬件开发工程师哦。

(就是这个玩意)

所以要学汇编、学C(事实上我没学好,也能做个计时器啥的...) ,然后放弃了,这个不太好找工作。 然后学Java去了(因为上网找C语言资料的时候),学了一段时间后,大概到SSH框架吧,15年那时候来过一次杭州,面了一个做Java开发的,给了3500的工资(没去,那时候正在跟女朋友谈婚阶段了)。 后面16年初的时候接触了web前端(因为JSP要写HTML,查资料的时候web前端培训的广告挺火的),慎重考虑后,16年6月辞了工作(已经结婚了,老婆也支持),来杭州报了一个线下的培训班学习4个月,挺贵的,我还是觉得自己没学好。然后10月底的时候拿了个7K的offer,感觉月薪过万有希望了。 我就正式入坑了...^_^

四年的工作经历

第一份工作

16年10月份找工作,面了4家公司拿了2个offer,选择了这家做医疗检验的。工作了差不多三年。一开始去公司,改.net项目样式。后面前后端分离用JQ写项目。然后用Vue全家桶来做项目。这个阶段技术成长了很多。 也组建了一个8人的前端团队(特别感谢老板的信任,当初我买房还找他借了20多万周转,现在还欠着这个人情呢)。后面有想法做了一些工程化的工作,对于前端基建方面没有任何认知,团队管理方面也没有管理知识的支撑。

第二份工作

出于提升自己的目的来到这家公司的,结果跟我想的不太一样(虽然公司大了些,开发跟第一家公司也差不多),这一年成长太少了。好在学了算法。参加了早早聊,打开了视野。也在团队中也做了几次分享。

第三份工作

本打算好好工作的,奈何天不遂人愿,被裁了(为了融资扩招,然后裁员,找工作真的要擦亮眼啊)。三个多月的时间,简历被搞花了....

第四份工作(还在面试中......)

可能会选择去大一点的团队吧。

2020,我到底干了啥?

工作

7月份之前,主要还是写业务代码,不过从年初到7月,大概面试了有小百人吧,这个对我来说收获还是蛮大的。接触了从应届到工作10年工作经验、大小厂的各种前端大小朋友。最大的感觉就是:

  • 工作年限不等于工作能力
  • 平台牛不代表你牛
  • 基础任何阶段都不能拉下

7月份换工作的时候也不是我本意,本来是申请回到总部办公的。由于其他原因变成了离职。7月底的时候来了新公司(当时Scott建议我去丁香园的,考虑距离问题选择了这家),然后11月被裁了(为了融资扩招,可怜我试用期还没过呢)。内心委屈,毕竟是想好好上班的...
11月到现在,面试了9家大型互联网公司,暂时没有拿到offer,但是很有信心,我一定可以。 这个月面试的感触:

  • 基础知识要夯实
  • 要有前端广阔的视野,后端要有一定的认知
  • 要有某一方向的技术深度
  • 性能优化必问
  • 算法一定要刷

学习

这段时间刷了很多大厂面试题,做了一个整理,也是为了方便自己复习,有空就可以拿出来刷一刷。

HTML 和 CSS
  • 你如何理解 HTML 结构的语义化?
  • 谈谈以前端角度出发做好 SEO 需要考虑什么?
  • 有哪项方式可以对一个 DOM 设置它的 CSS 样式?
  • CSS 都有哪些选择器?
  • CSS 中可以通过哪些属性定义,使得一个 DOM 元素不显示在浏览器可视范围内?
  • 超链接访问过后 hover 样式就不出现的问题是什么?如何解决?
  • 什么是 Css Hack?ie6,7,8 的 hack 分别是什么?
  • 请用 Css 写一个简单的幻灯片效果页面
  • 行内元素和块级元素的具体区别是什么?行内元素的padding和margin可设置吗?
  • 什么是外边距重叠?重叠的结果是什么

......

JS基础
  • call 和 apply 的区别
  • b 继承 a 的方法
  • JavaScript this 指针、闭包、作用域
  • 事件委托是什么
  • 闭包是什么,有什么特性,对页面有什么影响
  • 如何阻止事件冒泡和默认事件
  • 添加 删除 替换 插入到某个接点的方法
  • javascript 的本地对象,内置对象和宿主对象
  • document load 和 document ready 的区别
  • “==”和“===”的不同
  • javascript 的同源策略
  • 编写一个数组去重的方法

Ajax
  • Ajax 是什么? 如何创建一个 Ajax?
  • 同步和异步的区别?
  • 如何解决跨域问题?
  • 页面编码和被请求的资源编码如果不一致如何处理?
  • 简述 ajax 的过程。
  • 阐述一下异步加载。
  • 请解释一下 JavaScript 的同源策略。
  • GET 和 POST 的区别,何时使用 POST?
  • Ajax 的最大的特点是什么。
  • ajax 请求的时候 get 和 post 方式的区别
  • 解释 jsonp 的原理,以及为什么不是真正的 ajax
  • http 常见的状态码有那些?分别代表是什么意思?
  • 一个页面从输入 URL 到页面加载显示完成,这个过程中都发生了什么?

......

JS高级
  • JQuery 一个对象可以同时绑定多个事件,这是如何实现的?
  • 知道什么是 webkit 么? 知道怎么用浏览器的各种工具来调试和 debug 代码么?
  • 如何测试前端代码么? 知道 BDD, TDD, Unit Test 么? 知道怎么测试你的前端工程么(mocha, sinon, jasmin, qUnit..)
  • 前端 templating(Mustache, underscore, handlebars)是干嘛的, 怎么用?
  • 简述一下 Handlebars 的基本用法?
  • 简述一下 Handlerbars 的对模板的基本处理流程, 如何编译的?如何缓存的?
  • 用 js 实现千位分隔符?
  • 检测浏览器版本版本有哪些方式?
  • 我们给一个 dom 同时绑定两个点击事件,一个用捕获,一个用冒泡,你来说下会执
  • 行几次事件,然后会先执行冒泡还是捕获

......

Vue
  • vuex 有哪几种属性?
  • vuex 的 State 特性是?
  • vuex 的 Getter 特性是?
  • vuex 的 Mutation 特性是?
  • Vue.js 中 ajax 请求代码应该写在组件的 methods 中还是 vuex 的 actions 中?
  • 什么是 MVVM?
  • mvvm 和 mvc 区别?它和其它框架(jquery)的区别是什么?哪些场景适合?
  • vue 的优点是什么?
  • 组件之间的传值?
  • vue.cli 中怎样使用自定义的组件?有遇到过哪些问题吗?
  • vue 如何实现按需加载配合 webpack 设置
  • Vue 中引入组件的步骤?
  • 指令 v-el 的作用是什么?
  • 在 Vue 中使用插件的步骤
  • vue 生命周期的作用是什么
  • vue 生命周期总共有几个阶段
  • 第一次页面加载会触发哪几个钩子
  • DOM 渲染在 哪个周期中就已经完成
  • 简单描述每个周期具体适合哪些场

浏览器
  • 跨标签页通讯
  • 浏览器架构
  • 浏览器下事件循环(Event Loop)
  • 从输入 url 到展示的过程
  • 重绘与回流
  • 存储
  • Web Worker
  • V8 垃圾回收机制
  • 内存泄露
  • reflow(回流)和 repaint(重绘)优化
  • 如何减少重绘和回流?
  • 一个页面从输入 URL 到页面加载显示完成,这个过程中都发生了什么?
  • localStorage 与 sessionStorage 与 cookie 的区别总结
  • 浏览器如何阻止事件传播,阻止默认行为

生活

平平淡淡,两个姑娘越来越漂亮了 这个月没上班,接送大姑娘上下学、去培训班。现在的小孩子要学的可真多,我3岁多的时候应该在捏泥巴吧。 遗憾的是,因为疫情的原因没能带老婆孩子出去玩玩,明年补上吧。

2021,一定会更好的

许多事情要提上日程了,健身、英语、算法、理财、旅游、装修........想做的事情很多。

  • [ ] 多写一些总结,多复盘【每月输出2篇文档】
  • [ ] 算法【每日打卡,要弄懂】
  • [ ] 一次旅游 【但愿疫情彻底过去吧】
  • [ ] 读书【尽量每月1本吧】
  • [ ] 装修【房子交付要装修了】
  • [ ] 健身 【动起来,体检好几项都不行,得关注一下身体健康了】
  • [ ] 英语【背背单词】
  • [ ] 理财 【暂定吧】
  • [ ] ....

2021年,走的慢一点没关系,千万不能停!未来一定会更好的。

最后

前端工作四年多点了,好在没有放弃。虽然离优秀还很远,但贵在一直坚持。技术慢慢在提升、视野越来越开阔、薪水也比刚做前端时多了3倍多,我没法跟那些优秀的人比,我只能跟自己比,今天的我比昨天的我进步了一点就很开心了。 2021年,一定要读完的3本书《刻意练习》、《复盘》、《戒了吧,拖延症》。 愿自己越来越好。

查看原文

赞 21 收藏 7 评论 9

fsme 提出了问题 · 2020-12-17

Flutter 有的函数的参数为什么为常量?

例如:

BorderSide BorderSide({
 Color color = const Color(0xFF000000), 
 double width = 1.0, 
 BorderStyle style = BorderStyle.solid
})

BorderSide 这个内置函数中 color的值就是 const; const 声明的值不是常量么,这样写的意义是什么

关注 2 回答 1

fsme 发布了文章 · 2020-11-03

Charles windows10 配置HTTPS抓包证书配置问题

如果已经安装证书

cmd下执行 certmgr.msc
可以看到当前所有的证书
image.png

看是否安装到这个文件夹下
如果没有,安装时记得选择:
image.png 安装到这个位置

查看原文

赞 0 收藏 0 评论 0

fsme 赞了文章 · 2020-09-22

⚡️前端多线程大文件下载实践,提速10倍,拿捏百度云盘

背景

没错,你没有看错,是前端多线程,而不是Node。这一次的探索起源于最近开发中,有遇到视频流相关的开发需求发现了一个特殊的状态码,他的名字叫做 206~

屏幕快照 2020-09-21 23.21.05

为了防止本文的枯燥,先上效果图镇文。(以一张3.7M 大小的图片为例)。

动画效果对比(单线程-左 VS 10个线程-右)

single-vs-multiple-donwload

时间对比(单线程 VS 10个线程)

image-20200915235421355

看到这里是不是有点心动,那么请你继续听我道来,那我们先抓个包来看看整个过程是怎么发生的。

`GET /360_0388.jpg HTTP/1.1
Host: limit.qiufeng.com
Connection: keep-alive
...
Range: bytes=0-102399

HTTP/1.1 206 Partial Content
Server: openresty/1.13.6.2
Date: Sat, 19 Sep 2020 06:31:11 GMT
Content-Type: image/jpeg
Content-Length: 102400
....
Content-Range: bytes 0-102399/3670627

...(这里是文件流)` 

可以看到请求这里多出一个字段 Range: bytes=0-102399 ,服务端也多出一个字段Content-Range: bytes 0-102399/3670627,以及返回的 状态码为 206.

那么Range是什么呢?还记得前几天写过一篇文章,是关于文件下载的,其中有提到大文件的下载方式,有个叫 Range的东西,但是上一篇作为系统性地介绍文件下载的概览,因此没有对range 进行详细介绍。

以下所有代码均在 https://github.com/hua1995116/node-demo/tree/master/file-download/example/download-multiple

Range 基本介绍

Range的起源

Range是在 HTTP/1.1 中新增的一个字段,这个特性也是我们使用的迅雷等支持多线程下载以及断点下载的核心机制。(介绍性的文案,摘录了一下)

首先客户端会发起一个带有Range: bytes=0-xxx的请求,如果服务端支持 Range,则会在响应头中添加Accept-Ranges: bytes来表示支持 Range 的请求,之后客户端才可能发起带 Range 的请求。

服务端通过请求头中的Range: bytes=0-xxx 来判断是否是进行 Range 处理,如果这个值存在而且有效,则只发回请求的那部分文件内容,响应的状态码变成206,表示Partial Content,并设置Content-Range。如果无效,则返回416状态码,表明Request Range Not Satisfiable。如果请求头中不带 Range,那么服务端则正常响应,也不会设置 Content-Range 等。

image.png

Range的格式为:

Range:(unit=first byte pos)-[last byte pos]

Range: 单位(如bytes)= 开始字节位置-结束字节位置

我们来举个例子,假设我们开启了多线程下载,需要把一个5000byte的文件分为4个线程进行下载。

  • Range: bytes=0-1199 头1200个字节
  • Range: bytes=1200-2399 第二个1200字节
  • Range: bytes=2400-3599 第三个1200字节
  • Range: bytes=3600-5000 最后的1400字节

服务器给出响应:

第1个响应

  • Content-Length:1200
  • Content-Range:bytes 0-1199/5000

第2个响应

  • Content-Length:1200
  • Content-Range:bytes 1200-2399/5000

第3个响应

  • Content-Length:1200
  • Content-Range:bytes 2400-3599/5000

第4个响应

  • Content-Length:1400
  • Content-Range:bytes 3600-5000/5000

如果每个请求都成功了,服务端返回的response头中有一个 Content-Range 的字段域,Content-Range 用于响应头,告诉了客户端发送了多少数据,它描述了响应覆盖的范围和整个实体长度。一般格式:

Content-Range: bytes (unit first byte pos) - [last byte pos]/[entity length]Content-Range:字节 开始字节位置-结束字节位置/文件大小

浏览器支持情况

主流浏览器目前都支持这个特性。

image-20200916002624861

服务器支持

Nginx

在版本nginx版本 1.9.8 后,(加上 ngx_http_slice_module)默认自动支持,可以将 max_ranges 设置为 0的来取消这个设置。

Node

Node 默认不提供 对 Range 方法的处理,需要自己写代码进行处理。

router.get('/api/rangeFile', async(ctx) => {
    const { filename } = ctx.query;
    const { size } = fs.statSync(path.join(__dirname, './static/', filename));
    const range = ctx.headers['range'];
    if (!range) {
        ctx.set('Accept-Ranges', 'bytes');
        ctx.body = fs.readFileSync(path.join(__dirname, './static/', filename));
        return;
    }
    const { start, end } = getRange(range);
    if (start >= size || end >= size) {
        ctx.response.status = 416;
        ctx.body = '';
        return;
    }
    ctx.response.status = 206;
    ctx.set('Accept-Ranges', 'bytes');
    ctx.set('Content-Range', `bytes ${start}-${end ? end : size - 1}/${size}`);
    ctx.body = fs.createReadStream(path.join(__dirname, './static/', filename), { start, end });
}) 

或者你可以使用 koa-send 这个库。

https://github.com/pillarjs/send/blob/0.17.1/index.js#L680

Range实践

架构总览

我们先来看下流程架构图总览。单线程很简单,正常下载就可以了,不懂的可以参看我上一篇文章。多线程的话,会比较麻烦一些,需要按片去下载,下载好后,需要进行合并再进行下载。(关于blob等下载方式依旧可以参看上一篇

1600705973008

服务端代码

很简单,就是对Range做了兼容。

router.get('/api/rangeFile', async(ctx) => {
    const { filename } = ctx.query;
    const { size } = fs.statSync(path.join(__dirname, './static/', filename));
    const range = ctx.headers['range'];
    if (!range) {
        ctx.set('Accept-Ranges', 'bytes');
        ctx.body = fs.readFileSync(path.join(__dirname, './static/', filename));
        return;
    }
    const { start, end } = getRange(range);
    if (start >= size || end >= size) {
        ctx.response.status = 416;
        ctx.body = '';
        return;
    }
    ctx.response.status = 206;
    ctx.set('Accept-Ranges', 'bytes');
    ctx.set('Content-Range', `bytes ${start}-${end ? end : size - 1}/${size}`);
    ctx.body = fs.createReadStream(path.join(__dirname, './static/', filename), { start, end });
}) 

html

然后来编写 html ,这没有什么好说的,写两个按钮来展示。

<!-- html -->
<button id="download1">串行下载</button>
<button id="download2">多线程下载</button>
<script data-original="https://cdn.bootcss.com/axios/0.19.2/axios.min.js"></script> 

js公共参数

const m = 1024 * 520;  // 分片的大小
const url = 'http://localhost:8888/api/rangeFile?filename=360_0388.jpg'; // 要下载的地址 

单线程部分

单线程下载代码,直接去请求以blob方式获取,然后用blobURL 的方式下载。

download1.onclick = () => {
    console.time("直接下载");
    function download(url) {
        const req = new XMLHttpRequest();
        req.open("GET", url, true);
        req.responseType = "blob";
        req.onload = function (oEvent) {
            const content = req.response;
            const aTag = document.createElement('a');
            aTag.download = '360_0388.jpg';
            const blob = new Blob([content])
            const blobUrl = URL.createObjectURL(blob);
            aTag.href = blobUrl;
            aTag.click();
            URL.revokeObjectURL(blob);
            console.timeEnd("直接下载");
        };
        req.send();
    }
    download(url);
} 

多线程部分

首先发送一个 head 请求,来获取文件的大小,然后根据 length 以及设置的分片大小,来计算每个分片是滑动距离。通过Promise.all的回调中,用concatenate函数对分片 buffer 进行一个合并成一个 blob,然后用blobURL 的方式下载。

// script
function downloadRange(url, start, end, i) {
    return new Promise((resolve, reject) => {
        const req = new XMLHttpRequest();
        req.open("GET", url, true);
        req.setRequestHeader('range', `bytes=${start}-${end}`)
        req.responseType = "blob";
        req.onload = function (oEvent) {
            req.response.arrayBuffer().then(res => {
                resolve({
                    i,
                    buffer: res
                });
            })
        };
        req.send();
    })
}
// 合并buffer
function concatenate(resultConstructor, arrays) {
    let totalLength = 0;
    for (let arr of arrays) {
        totalLength += arr.length;
    }
    let result = new resultConstructor(totalLength);
    let offset = 0;
    for (let arr of arrays) {
        result.set(arr, offset);
        offset += arr.length;
    }
    return result;
}
download2.onclick = () => {
    axios({
        url,
        method: 'head',
    }).then((res) => {
        // 获取长度来进行分割块
        console.time("并发下载");
        const size = Number(res.headers['content-length']);
        const length = parseInt(size / m);
        const arr = []
        for (let i = 0; i < length; i++) {
            let start = i * m;
            let end = (i == length - 1) ?  size - 1  : (i + 1) * m - 1;
            arr.push(downloadRange(url, start, end, i))
        }
        Promise.all(arr).then(res => {
            const arrBufferList = res.sort(item => item.i - item.i).map(item => new Uint8Array(item.buffer));
            const allBuffer = concatenate(Uint8Array, arrBufferList);
            const blob = new Blob([allBuffer], {type: 'image/jpeg'});
            const blobUrl = URL.createObjectURL(blob);
            const aTag = document.createElement('a');
            aTag.download = '360_0388.jpg';
            aTag.href = blobUrl;
            aTag.click();
            URL.revokeObjectURL(blob);
            console.timeEnd("并发下载");
        })
    })
} 

完整示例

https://github.com/hua1995116/node-demo
`// 进入目录
cd file-download
// 启动
node server.js
// 打开 
http://localhost:8888/example/download-multiple/index.html` 

由于谷歌浏览器在 HTTP/1.1 对于单个域名有所限制,单个域名最大的并发量是 6.

这一点可以在源码以及官方人员的讨论中体现。

讨论地址

https://bugs.chromium.org/p/chromium/issues/detail?id=12066

Chromium 源码

// https://source.chromium.org/chromium/chromium/src/+/refs/tags/87.0.4268.1:net/socket/client_socket_pool_manager.cc;l=47
// Default to allow up to 6 connections per host. Experiment and tuning may
// try other values (greater than 0).  Too large may cause many problems, such
// as home routers blocking the connections!?!?  See http://crbug.com/12066.
//
// WebSocket connections are long-lived, and should be treated differently
// than normal other connections. Use a limit of 255, so the limit for wss will
// be the same as the limit for ws. Also note that Firefox uses a limit of 200.
// See http://crbug.com/486800
int g_max_sockets_per_group[] = {
    6,   // NORMAL_SOCKET_POOL
    255  // WEBSOCKET_SOCKET_POOL
}; 

因此为了配合这个特性我将文件分成6个片段,每个片段为520kb (没错,写个代码都要搞个爱你的数字),即开启6个线程进行下载。

我用单个线程和多个线程进行分别下载了6次,看上去速度是差不多的。那么为什么和我们预期的不一样呢?

image-20200919165242745

探索失败的原因

我开始仔细对比两个请求,观察这两个请求的速度。

6个线程并发

image-20200919170313455

单个线程

image-20200919170512650

我们按照3.7M 82ms 的速度来算的话,大约为 1ms 下载 46kb,而实际情况可以看到,533kb ,平均就要下载 20ms 左右(已经刨去了连接时间,纯 content 下载时间)。

我就去查找了一些资料,明白了有个叫做下行速度和上行速度的东西。

网络的实际传输速度要分上行速度和下行速度,上行速率就是发送出去数据的速度,下行就是收到数据的速度。ADSL是根据我们平时上网,发出数据的要求相对下载数据的较小这种习惯来实现的一种传输方式。我们说对于4M的宽带,那么我们的l理论最高下载速度就是512K/S,这就是所说的下行速度。 --百度百科

那我们现在的情况是怎么样的呢?

把服务器比作一根大水管,我来用图模拟一下我们单个线程和多个线程下载的情况。左侧为服务器端,右侧为客户端。(以下所有情况都是考虑理想情况下,只是为了模拟过程,不考虑其他一些程序的竞态影响。)

单线程

IMG_01

多线程

IMG_02

没错,由于我们的服务器是一根大水管,流速是一定的,并且我们客户端没有限制。如果是单线程跑的话,那么会跑满用户的最大的速度。如果是多线程呢,以3个线程为例子的话,相当于每个线程都跑了原先线程三分之一的速度。合起来的速度和单个线程是没有差别的。

下面我就分几种情况来讲解一下,什么样的情况才我们的多线程才会生效呢?

服务器带宽大于用户带宽,不做任何限制

这种情况其实我们遇到的情况差不多的。

服务器带宽远大于用户带宽,限制单连接网速

IMG_03

如果服务器限制了单个宽带的下载速度,大部分也是这种情况,例如百度云就是这样,例如明明你是 10M 的宽带,但是实际下载速度只有 100kb/s ,这种情况下,我们就可以开启多线程去下载,因为它往往限制的是单个TCP的下载,当然在线上环境不是说可以让用户开启无限多个线程,还是会有限制的,会限制你当前IP的最大TCP。这种情况下下载的上限往往是你的用户最大速度。按照上面的例子,如果你开10个线程已经达到了最大速度,因为再大,你的入口已经被限制死了,那么各个线程之间就会抢占速度,再多开线程也没有用了。

改进方案

由于 Node 我暂时没有找到比较简单地控制下载速度的方法,因此我就引入了 Nginx。

我们将每个TCP连接的速度控制在 1M/s。

加入配置 limit_rate 1M;

准备工作

1.nginx_conf

server {
    listen 80;
    server_name limit.qiufeng.com;
    access_log  /opt/logs/wwwlogs/limitqiufeng.access.log;
    error_log  /opt/logs/wwwlogs/limitqiufeng.error.log;

    add_header Cache-Control max-age=60;
    add_header Access-Control-Allow-Origin *;
    add_header Access-Control-Allow-Methods 'GET, OPTIONS';
    add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization,range,If-Range';
    if ($request_method = 'OPTIONS') {
        return 204;
    }
    limit_rate 1M;
    location / {
        root 你的静态目录;
        index index.html;
    }
} 

2.配置本地 host

`127.0.0.1 limit.qiufeng.com` 

查看效果,这下基本上速度已经是正常了,多线程下载比单线程快了速度。基本是 5-6 : 1 的速度,但是发现如果下载过程中快速点击数次后,使用Range下载会越来越快(此处怀疑是 Nginx 做了什么缓存,暂时没有深入研究)。

修改代码中的下载地址
const url = 'http://localhost:8888/api/rangeFile?filename=360_0388.jpg';
变成
const url = 'http://limit.qiufeng.com/360_0388.jpg'; 

测试下载速度

image-20200919201613507

还记得上面说的吗,关于 HTTP/1.1 同一站点只能并发 6 个请求,多余的请求会放到下一个批次。但是 HTTP/2.0 不受这个限制,多路复用代替了 HTTP/1.x序列和阻塞机制。让我们来升级 HTTP/2.0 来测试一下。

需要本地生成一个证书。(生成证书方法: https://juejin.im/post/6844903556722475021)

server {
    listen 443 ssl http2;
    ssl on;
    ssl_certificate /usr/local/openresty/nginx/conf/ssl/server.crt;
    ssl_certificate_key /usr/local/openresty/nginx/conf/ssl/server.key;
    ssl_session_cache shared:le_nginx_SSL:1m;
    ssl_session_timeout 1440m;

    ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers RC4:HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;
    server_name limit.qiufeng.com;
 
    access_log  /opt/logs/wwwlogs/limitqiufeng2.access.log;
    error_log  /opt/logs/wwwlogs/limitqiufeng2.error.log;

    add_header Cache-Control max-age=60;
    add_header Access-Control-Allow-Origin *;
    add_header Access-Control-Allow-Methods 'GET, OPTIONS';
    add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization,range,If-Range';
    if ($request_method = 'OPTIONS') {
        return 204;
    }
    limit_rate 1M;
    location / {
        root 你存放项目的前缀路径/node-demo/file-download/;
        index index.html;
    }
} 

10个线程

`将单个下载大小进行修改
const m = 1024 * 400;` 

image-20200919200203877

12个线程

image-20200919202302096

24个线程

image-20200919202138838

当然线程不是越多越好,经过测试,发现线程达到一定数量的时候,反而速度会更加缓慢。以下是 36个并发请求的效果图。

image-20200919202427985

实际应用探索

那么多进程下载到底有啥用呢?没错,开头也说了,这个分片机制是迅雷等下载软件的核心机制。

网易云课堂

https://study.163.com/course/courseLearn.htm?courseId=1004500008#/learn/video?lessonId=1048954063&courseId=1004500008

我们打开控制台,很容易地发现这个下载 url,直接一个裸奔的 mp4 下载地址。

image-20200920222053726

把我们的测试脚本从控制台输入进行。

// 测试脚本,由于太长了,而且如果仔细看了上面的文章也应该能写出代码。实在写不出可以看以下代码。
https://github.com/hua1995116/node-demo/blob/master/file-download/example/download-multiple/script.js 

直接下载

image-20200920221657541

多线程下载

image-20200920221853959

可以看到由于网易云课堂对单个TCP的下载速度并没有什么限制没有那么严格,提升的速度不是那么明显。

百度云

我们就来测试一下网页版的百度云。

image-20200919210106839

以一个 16.6M的文件为例。

打开网页版百度云盘的界面,点击下载

image-20200920222309345

这个时候点击暂停, 打开 chrome -> 更多 -> 下载内容 -> 右键复制下载链接

image-20200922004619680

依旧用上述的网易云课程下载课程的脚本。只不过你需要改一下参数。

`url 改成对应百度云下载链接
m 改成 1024 * 1024 * 2 合适的分片大小~` 

直接下载

百度云多单个TCP连接的限速,真的是惨无人道,足足花了217秒!!!就一个17M的文件,平时我们饱受了它多少的折磨。(除了VIP玩家)

image-20200919211105023

多线程下载

image-20200919210516632

由于是HTTP/1.1 因此我们只要开启6个以及以上的线程下载就好了。以下是多线程下载的速度,约用时 46 秒。

image-20200919210550840

我们通过这个图再来切身感受一下速度差异。

image-20200922010911389

真香,免费且只靠我们前端自己实现了这个功能,太tm香了,你还不赶紧来试试??

方案缺陷

1.对于大文件的上限有一定的限制

由于 blob 在 各大浏览器有上限大小的限制,因此该方法还是存在一定的缺陷。

image.png

2. 服务器对单个TCP速度有所限制

一般情况下都会有限制,那么这个时候就看用户的宽度速度了。

结尾

文章写的比较仓促,表达可能不是特别精准,如有错误之处,欢迎各位大佬指出。

回头调研下,有没有网页版百度云加速的插件,如果没有就造一个网页版百度云下载的插件~。

系列文章

参考文献

Nginx带宽控制 : https://blog.huoding.com/2015/03/20/423

openresty 部署 https 并开启 http2 支持 : https://www.gryen.com/articles/show/5.html

聊一聊HTTP的Range : https://dabing1022.github.io/2016/12/24/聊一聊HTTP的Range, Content-Range/

最后

如果我的文章有帮助到你,希望你也能帮助我,欢迎关注我的微信公众号 秋风的笔记,回复好友 二次,可加微信并且加入交流群,秋风的笔记 将一直陪伴你的左右。

image

查看原文

赞 97 收藏 68 评论 7

fsme 回答了问题 · 2020-09-16

vue 打包插件包太大已经2m,如何优化?

webpack 配置 externals, 把SDK放到CDN引入,不要打包到公共文件中试试呢,还有 vue 全家桶都可以使用cdn引入,可以极大的减小包体积

关注 3 回答 2

fsme 回答了问题 · 2020-09-14

解决angular6项目,有30个请求,根据用户配置抽取n个请求发送,待收到全部结果后执行后续操作。

用 Promise 实现呢

关注 3 回答 3

fsme 回答了问题 · 2020-09-14

解决vue的动态组件, 每个组件都有不同的slot怎么处理?

slot 是可以具名的,只要这几个不要重名就好

关注 2 回答 1

fsme 关注了用户 · 2020-09-02

凹凸实验室 @o2team

凹凸实验室(Aotu.io,英文简称O2) 始建于2015年10月,是一个年轻基情的技术团队。

O2面向多终端技术体系,致力于构建沉淀与分享包括但不限于交互、页面制作技巧、前端开发、原生APP开发等方面的专业知识及案例。

求简历:aotu@jd.com

关注 779

fsme 关注了问题 · 2020-09-02

解决美团前端二面,读代码题求解

var a = 0;
if (true) {
    a = 1;
    function a() {}
    a = 21;
    console.log(a);
}
console.log(a);

求两次 console 出来的值。

答案是21 1。

本菜鸡想了很久也没想明白第二个为啥是1。。

求大佬解惑 ?

关注 12 回答 6

认证与成就

  • 获得 72 次点赞
  • 获得 17 枚徽章 获得 1 枚金徽章, 获得 4 枚银徽章, 获得 12 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

  • uni-appshowModal

    适用于uni-app 的跨端显示弹层,使用接口参数与uni-app 中的showModal参数一致

注册于 2017-12-26
个人主页被 1.4k 人浏览