【计算机网络】浏览器组成、工作原理、页面渲染流程...
1、一个页面从输入URL到页面加载完的过程中都发生了什么?
不分析硬件层面,一个简化的过程:
① 在接收到用户输入的网址后,浏览器会开启一个线程来处理这个请求,对用户输入的URL地址进行分析判断,如果是HTTP协议就按照HTTP方式来处理。
② 调用浏览器引擎中的对应方法,比如WebView中的loadUrl方法,分析并加载这个URL地址。
③ 通过DNS解析获取该网站地址对应的IP地址,查询完成后连同浏览器的Cookie、userAgent等信息向网站目的IP发出请求。
④ 进行HTTP协议会话,浏览器客户端向Web服务器发送报文。
⑤ 进入网站后台上的Web服务器处理请求,如Apache、Tomcat、Nginx、Node.js 等服务器。
⑥ 进入部署好的后端应用,如PHP、Java、 JavaScript、 Python 等后端程序,找到对应的请求处理逻辑,这期间可能会读取服务器缓存或查询数据库等。
⑦ 服务器处理请求并返回响应报文,此时如果浏览器访问过该页面,缓存上有对应资源,会与服务器最后修改记录对比,一致则返回304,否则返回200和对应的内容。
⑧ 浏览器开始下载HTML文档(响应报头状态码为200时)或者从本地缓存读取文件内容(浏览器缓存有效或响应报头状态码为304时)。
⑨ 浏览器根据下载接收到的HTML文件解析结构建立DOM文档树,并根据HTML中的标记请求下载指定的MIME类型文件(如CSS、JavaScript 脚本等),同时设置缓存等内容。
⑩ 页面开始解析渲染DOM、CSS根据规则解析并结合DOM文档树进行网页内容布局和绘制渲染,JavaScript 根据DOM API操作DOM,并读取浏览器缓存、执行事件绑定等,页面整个展示过程完成。
服务器端的展开:
2、浏览器组成部分
用户界面 (User Interface) | 包括地址栏、前进/后退按钮、书签菜单等。除了浏览器主窗口显示请求的页面外,其他显示的各个部分都属于用户界面。 |
---|---|
浏览器引擎 (Browser engine) | 在用户界面和呈现引擎之间传送指令,或者在客户端本地缓存中读写数据等,是浏览器中各个部分之间相互通信的核心。 |
呈现(渲染)引擎 (Rendering engine) | 负责显示请求的内容,并将内容排版到浏览器中显示成有样式的界面。如果请求的内容是 HTML,它就负责解析 HTML 和 CSS 内容,并将解析后的内容显示在屏幕上,也叫排版引擎,常说的浏览器内核一般也主要是指呈现引擎。 |
网络功能模块 (Networking) | 用于网络调用,比如 HTTP 请求。它的接口与平台无关,并为所有平台提供底层实现。 |
用户界面后端 (UI Backend) | 用于绘制基本的窗口小部件,比如组合框和窗口。它公开了与平台无关的通用接口,而在底层使用操作系统的用户界面方法。 |
JavaScript 解释器 (Javascript Interpreter) | 用于解析和执行 JavaScript 代码。 |
数据存储 (Data Persistence) | 持久层。浏览器需要在硬盘上保存各种数据,例如 Cookie、localStorage。新的 HTML 规范 (HTML5) 定义了“网络数据库”,这是一个完整(但是轻便)的浏览器内数据库。 |
3、浏览器内核
浏览器的主要功能是将用户选择得web资源呈现出来,它需要从服务器请求资源,并将其显示在浏览器窗口中,资源的格式通常是HTML,也包括PDF、image及其他格式。
浏览器内核的主要作用,是帮助浏览器来渲染网页的内容,负责解析网页语法(如html、javascript),并渲染、展示网页。将页面内容和排版代码转换为用户所见的视图。
内核分为 渲染引擎 和 js引擎 两个部分。
由于 js引擎 越来越独立,所以内核倾向于指的是渲染引擎。
3.1 渲染引擎
作用是:负责获取网页内容(html、图像)、整理消息、计算网页显示方式、输出到显示器这些工作
渲染引擎内含:
- html解释器:将html文本解析成dom树(文档对象模型)
- css解释器:为dom对象计算样式信息,为计算机布局提供基础设施
- layout布局:在dom建立完毕后,计算出他们的位置大小、布局信息。形成一个内部表示模型。
3.2 js引擎
作用是:解析js语言,执行js语言。并通过 DOM 接口和 CSSOM 接口修改布局和样式。实现网页的动态交互效果。
不同的浏览器有不同的内核,这就导致了不同的浏览器对同一段代码的解析结果不一致。
3.3 主流浏览器内核
内核 | 是否开源 | 插件支持 | 应用浏览器 | 支持操作系統 |
---|---|---|---|---|
Trident | 否,但提供接口调用 | ActiveX | IE | Windows |
Gecko | 是,多种协议授权发行,包括MPL、GPL、LGPL | NPAP | Firefox | Windows,Mac,Linux/BSD |
Blink | 是 | NPAPI | Chrome, Opera | Windows,Mac,Linux/BSD |
Webkit | 是,遵从LGPL协议 | NPAPI | Chrome, Safar | Windows,Mac,Linux/BSD |
4、渲染引擎
4.1 基本流程
渲染引擎首先通过网络获得所请求文档的内容,通常以8K分块的方式完成。
下面是渲染引擎在取得内容之后的基本流程:
解析html以构建dom树 -> 构建render树 -> 布局render树 -> 绘制render树
① 解析HTML构建DOM树时,呈现引擎会先将HTML元素标签解析成由多个DOM元素对象节点组成的且具有节点父子关系的DOM树结构(DOM tree)。
② 然后根据DOM树结构的每个节点顺序提取计算使用的CSS规则,并重新计算DOM树结构的样式数据,生成一个带样式描述的DOM渲染树(render tree)对象。
③ DOM渲染树生成结束后,进入渲染树的布局(Layout)阶段,即根据每个渲染树节点在页面中的大小和位置,将节点固定到页面的对应位置上,这个阶段主要是元素的布局属性(例如position、float、margin 等属性)生效,即在浏览器中绘制页面上元素节点的位置。
④ 接下来就是绘制阶段,将渲染树节点的背景、颜色、文本等样式信息应用到每个节点上,这个阶段主要是元素的内部显示样式(例如color、background. text-shadow 等属性)生效,最终完成整个DOM在页面上的绘制(Painting)显示。
值得注意的是,这个过程是逐步完成的,为了更好的用户体验,渲染引擎将会尽可能早的将内容呈现到屏幕上,并不会等到所有的html都解析完成之后再去构建和布局render树。它是解析完一部分内容就显示一部分内容,同时,可能还在通过网络下载其余内容。
4.2 webkit & Geoko
webkit 主流程:
Geoko 渲染引擎主流程:
两种呈现引擎工作流程的主要区别在于解析HTML或CSS文档生成呈现树的过程:
- Webkit 内核中的HTML和CSS解析可以认为是并行的;
- Gecko 则是先解析HTML,生成内容槽 (Content Sink)后再开始解析CSS。
这两种呈现引擎工作过程中使用的描述术语也不一样:
- Gecko称可见的格式化元素组成的树为frame树,每个元素都是一个frame,webkit则使用render树这个名词来命名由渲染对象组成的树。
- Webkit中元素的定位称为布局,而Gecko中称为回流。
- Webkit称利用dom节点及样式信息去构建render树的过程为attachment,Gecko在html和dom树之间附加了一层,这层称为内容接收器,相当制造dom元素的工厂。
但是它们主要的流程是相似的,都经过HTML DOM解析、CSS样式解析、呈现树生成和呈现树绘制显示阶段。一般呈现引擎的解析过程中都包含了HTML解析和CSS解析阶段,这也是呈现引擎解析流程中最重要的两个部分。
4.3 解析 Parsing-general
解析一个文档,即将其转换为具有一定意义的结构——编码可以理解和使用的东西。解析的结果通常是表达文档结构的节点树,称为解析树或语法树。
例如,解析“2+3-1”这个表达式,可能返回这样一棵树。
解析基于文档依据的语法规则——文档的语言或格式。每种可被解析的格式必须具有由词汇及语法规则组成的特定的文法Grammars,称为上下文无关文法。人类语言不具有这一特性,因此不能被一般的解析技术所解析。
解析可以分为两个子过程:
- 词法分析,将输入分解为符号,符号是语言的词汇表——基本有效单元的集合。对于人类语言来说,它相当于我们字典中出现的所有单词。
- 语法分析,指对语言应用语法规则。
从源文档到解析树:
解析过程是迭代的,解析器从词法分析器处取道一个新的符号,并试着用这个符号匹配一条语法规则,如果匹配了一条规则,这个符号对应的节点将被添加到解析树上,然后解析器请求另一个符号。如果没有匹配到规则,解析器将在内部保存该符号,并从词法分析器取下一个符号,直到所有内部保存的符号能够匹配一项语法规则。如果最终没有找到匹配的规则,解析器将抛出一个异常,这意味着文档无效或是包含语法错误。
4.4 HTML 解析
解析算法 The parsing algorithm
hmtl 不能被一般的解析器(自顶向下或自底向上)所解析,原因是:
- 这门语言本身的宽容特性
- 浏览器对一些常见的非法html有容错机制
- 解析过程是往复的,通常源码不会在解析过程中发生改变,但在html中,脚本标签包含的“document.write ”可能添加标签,这说明在解析过程中实际上修改了输入
- 不能使用正则解析技术,浏览器为html定制了专属的解析器
Html5规范中描述了这个解析算法,算法包括两个阶段——符号化及构建树。
符号化是词法分析的过程,将输入解析为符号,html的符号包括开始标签、结束标签、属性名及属性值。
符号识别器识别出符号后,将其传递给树构建器,并读取下一个字符,以识别下一个符号,这样直到处理完所有输入。
HTML 解析流程:
符号识别算法 The tokenization algorithm
算法输出html符号,该算法用状态机表示。每次读取输入流中的一个或多个字符,并根据这些字符转移到下一个状态,当前的符号状态及构建树状态共同影响结果,这意味着,读取同样的字符,可能因为当前状态的不同,得到不同的结果以进入下一个正确的状态。
这个算法很复杂,这里用一个简单的例子来解释这个原理。
基本示例——符号化下面的html:
<html>
<body>
Hello world
</body>
</html>
初始状态为“Data State”,当遇到“<”字符,状态变为“Tag open state”,读取一个a-z的字符将产生一个开始标签符号,状态相应变为“Tag name state”,一直保持这个状态直到读取到“>”,每个字符都附加到这个符号名上,例子中创建的是一个html符号。
当读取到“>”,当前的符号就完成了,此时,状态回到“Data state”,“”重复这一处理过程。到这里,html和body标签都识别出来了。
现在,回到“Data state”,读取“Hello world”中的字符“H”将创建并识别出一个字符符号,这里会为“Hello world”中的每个字符生成一个字符符号。
这样直到遇到“”中的“<”。现在,又回到了“Tag open state”,读取下一个字符“/”将创建一个闭合标签符号,并且状态转移到“Tag name state”,还是保持这一状态,直到遇到“>”。然后,产生一个新的标签符号并回到“Data state”。后面的“”将和“”一样处理。
符号化示例输入:
树的构建算法 Tree construction algorithm
在树的构建阶段,将修改以Document为根的DOM树,将元素附加到树上。每个由符号识别器识别生成的节点将会被树构造器进行处理,规范中定义了每个符号相对应的Dom元素,对应的Dom元素将会被创建。这些元素除了会被添加到Dom树上,还将被添加到开放元素堆栈中。这个堆栈用来纠正嵌套的未匹配和未闭合标签,这个算法也是用状态机来描述,所有的状态采用插入模式。
示例中树的创建过程:
<html>
<body>
Hello world
</body>
</html>
构建树这一阶段的输入是符号识别阶段生成的符号序列。
首先是“initial mode”,接收到html符号后将转换为“before html”模式,在这个模式中对这个符号进行再处理。此时,创建了一个HTMLHtmlElement元素,并将其附加到根Document对象上。
状态此时变为“before head”,接收到body符号时,即使这里没有head符号,也将自动创建一个HTMLHeadElement元素并附加到树上。
现在,转到“in head”模式,然后是“after head”。到这里,body符号会被再次处理,将创建一个HTMLBodyElement并插入到树中,同时,转移到“in body”模式。
然后,接收到字符串“Hello world”的字符符号,第一个字符将导致创建并插入一个text节点,其他字符将附加到该节点。
接收到body结束符号时,转移到“after body”模式,接着接收到html结束符号,这个符号意味着转移到了“after after body”模式,当接收到文件结束符时,整个解析过程结束。
示例html树的构建过程:
解析结束时的处理 Action when the parsing is finished
在这个阶段,浏览器将文档标记为可交互的,并开始解析处于延时模式中的脚本——这些脚本在文档解析后执行。
文档状态将被设置为完成,同时触发一个load事件。
Html5规范中有符号化及构建树的完整算法(http://www.w3.org/TR/html5/syntax.html#html-parser)。
浏览器容错 Browsers error tolerance
你从来不会在一个html页面上看到“无效语法”这样的错误,浏览器修复了无效内容并继续工作。
以下面这段html为例:
<html>
<mytag>
</mytag>
<div>
<p>
</div>
Really lousy HTML
</p>
</html>
这段html违反了很多规则(mytag不是合法的标签,p及div错误的嵌套等等),但是浏览器仍然可以没有任何怨言的继续显示,它在解析的过程中修复了html作者的错误。
浏览器都具有错误处理的能力,但是,另人惊讶的是,这并不是html最新规范的内容,就像书签及前进后退按钮一样,它只是浏览器长期发展的结果。一些比较知名的非法html结构,在许多站点中出现过,浏览器都试着以一种和其他浏览器一致的方式去修复。
Html5规范定义了这方面的需求,webkit在html解析类开始部分的注释中做了很好的总结。
解析器将符号化的输入解析为文档并创建文档,但不幸的是,我们必须处理很多没有很好格式化的html文档,至少要小心下面几种错误情况。
- 在未闭合的标签中添加明确禁止的元素。这种情况下,应该先将前一标签闭合
- 不能直接添加元素。有些人在写文档的时候会忘了中间一些标签(或者中间标签是可选的),比如HTML HEAD BODY TR TD LI等
- 想在一个行内元素中添加块状元素。关闭所有的行内元素,直到下一个更高的块状元素
- 如果这些都不行,就闭合当前标签直到可以添加该元素。
下面来看一些webkit容错的例子:
- 一些网站使用
</br>替代<br>
,为了兼容IE和Firefox,webkit将其看作<br>
。
代码:
Note-这里的错误处理在内部进行,用户看不到。if (t->isCloseTag(brTag) && m_document->inCompatMode()) { reportError(MalformedBRError); t->beginTag = true; }
- 迷路的表格,指一个表格嵌套在另一个表格中,但不在它的某个单元格内。
比如下面这个例子:
webkit将会将嵌套的表格变为两个兄弟表格:<table> <table> <tr><td>inner table</td></tr> </table> <tr><td>outer table</td></tr> </table>
代码:<table> <tr><td>outer table</td></tr> </table> <table> <tr><td>inner table</td></tr> </table>
webkit使用堆栈存放当前的元素内容,它将从外部表格的堆栈中弹出内部的表格,则它们变为了兄弟表格。if (m_inStrayTableContent && localName == tableTag) popBlock(tableTag);
- 嵌套的表单元素
用户将一个表单嵌套到另一个表单中,则第二个表单将被忽略。
代码:if (!m_currentFormElement) { m_currentFormElement = new HTMLFormElement(formTag, m_document); }
- 太深的标签继承
www.liceo.edu.mx是一个由嵌套层次的站点的例子,最多只允许20个相同类型的标签嵌套,多出来的将被忽略。
代码:bool HTMLParser::allowNestedRedundantTag(const AtomicString& tagName) { unsigned i = 0; for (HTMLStackElem* curr = m_blockStack; i < cMaxRedundantTagDepth && curr && curr->tagName == tagName; curr = curr->next, i++) { } return i != cMaxRedundantTagDepth; }
- 放错了地方的html、body闭合标签
支持不完整的html。我们从来不闭合body,因为一些愚蠢的网页总是在还未真正结束时就闭合它。我们依赖调用end方法去执行关闭的处理。
代码:if (t->tagName == htmlTag || t->tagName == bodyTag ) return;
4.5 CSS解析 CSS parsing
Webkit CSS 解析器 Webkit CSS parser
Webkit使用Flex和Bison解析生成器从CSS语法文件中自动生成解析器。Bison创建一个自底向上的解析器,Firefox使用自顶向下解析器。它们都是将每个css文件解析为样式表对象,每个对象包含css规则,css规则对象包含选择器和声明对象,以及其他一些符合css语法的对象。
解析 CSS:
脚本
web的模式是同步的,开发者希望解析到一个script标签时立即解析执行脚本,并阻塞文档的解析直到脚本执行完。如果脚本是外引的,则网络必须先请求到这个资源——这个过程也是同步的,会阻塞文档的解析直到资源被请求到。这个模式保持了很多年,并且在html4及html5中都特别指定了。开发者可以将脚本标识为defer,以使其不阻塞文档解析,并在文档解析结束后执行。Html5增加了标记脚本为异步的选项,以使脚本的解析执行使用另一个线程。
预解析 Speculative parsing
Webkit和Firefox都做了这个优化,当执行脚本时,另一个线程解析剩下的文档,并加载后面需要通过网络加载的资源。这种方式可以使资源并行加载从而使整体速度更快。需要注意的是,预解析并不改变Dom树,它将这个工作留给主解析过程,自己只解析外部资源的引用,比如外部脚本、样式表及图片。
样式表 Style sheets
样式表采用另一种不同的模式。理论上,既然样式表不改变Dom树,也就没有必要停下文档的解析等待它们,然而,存在一个问题,脚本可能在文档的解析过程中请求样式信息,如果样式还没有加载和解析,脚本将得到错误的值,显然这将会导致很多问题,这看起来是个边缘情况,但确实很常见。Firefox在存在样式表还在加载和解析时阻塞所有的脚本,而chrome只在当脚本试图访问某些可能被未加载的样式表所影响的特定的样式属性时才阻塞这些脚本。
渲染树的构造 Render tree construction
当Dom树构建完成时,浏览器开始构建另一棵树——渲染树。渲染树由元素显示序列中的可见元素组成,它是文档的可视化表示,构建这棵树是为了以正确的顺序绘制文档内容。
Firefox将渲染树中的元素称为frames,webkit则用renderer或渲染对象来描述这些元素。
一个渲染对象直到怎么布局及绘制自己及它的children。
RenderObject是Webkit的渲染对象基类,它的定义如下:
class RenderObject{
virtual void layout();
virtual void paint(PaintInfo);
virtual void rect repaintRect();
Node* node; //the DOM node
RenderStyle* style; // the computed style
RenderLayer* containgLayer; //the containing z-index layer
}
每个渲染对象用一个和该节点的css盒模型相对应的矩形区域来表示,正如css2所描述的那样,它包含诸如宽、高和位置之类的几何信息。盒模型的类型受该节点相关的display样式属性的影响(参考样式计算章节)。下面的webkit代码说明了如何根据display属性决定某个节点创建何种类型的渲染对象。
RenderObject* RenderObject::createObject(Node* node, RenderStyle* style)
{
Document* doc = node->document();
RenderArena* arena = doc->renderArena();
...
RenderObject* o = 0;
switch (style->display()) {
case NONE:
break;
case INLINE:
o = new (arena) RenderInline(node);
break;
case BLOCK:
o = new (arena) RenderBlock(node);
break;
case INLINE_BLOCK:
o = new (arena) RenderBlock(node);
break;
case LIST_ITEM:
o = new (arena) RenderListItem(node);
break;
...
}
return o;
}
元素的类型也需要考虑,例如,表单控件和表格带有特殊的框架。
在webkit中,如果一个元素想创建一个特殊的渲染对象,它需要复写“createRenderer”方法,使渲染对象指向不包含几何信息的样式对象。
渲染树和Dom树的关系 The render tree relation to the DOM tree
渲染对象和Dom元素相对应,但这种对应关系不是一对一的,不可见的Dom元素不会被插入渲染树,例如head元素。另外,display属性为none的元素也不会在渲染树中出现(visibility属性为hidden的元素将出现在渲染树中)。
还有一些Dom元素对应几个可见对象,它们一般是一些具有复杂结构的元素,无法用一个矩形来描述。例如,select元素有三个渲染对象——一个显示区域、一个下拉列表及一个按钮。同样,当文本因为宽度不够而折行时,新行将作为额外的渲染元素被添加。另一个多个渲染对象的例子是不规范的html,根据css规范,一个行内元素只能仅包含行内元素或仅包含块状元素,在存在混合内容时,将会创建匿名的块状渲染对象包裹住行内元素。
一些渲染对象和所对应的Dom节点不在树上相同的位置,例如,浮动和绝对定位的元素在文本流之外,在两棵树上的位置不同,渲染树上标识出真实的结构,并用一个占位结构标识出它们原来的位置。
渲染树及对应的Dom树:
创建树的流程 The flow of constructing the tree
Firefox中,表述为一个监听Dom更新的监听器,将frame的创建委派给Frame Constructor,这个构建器计算样式(参看样式计算)并创建一个frame。
Webkit中,计算样式并生成渲染对象的过程称为attachment,每个Dom节点有一个attach方法,attachment的过程是同步的,调用新节点的attach方法将节点插入到Dom树中。
处理html和body标签将构建渲染树的根,这个根渲染对象对应被css规范称为containing block的元素——包含了其他所有块元素的顶级块元素。它的大小就是viewport——浏览器窗口的显示区域,Firefox称它为viewPortFrame,webkit称为RenderView,这个就是文档所指向的渲染对象,树中其他的部分都将作为一个插入的Dom节点被创建。
样式计算 Style Computation
创建渲染树需要计算出每个渲染对象的可视属性,这可以通过计算每个元素的样式属性得到。
样式包括各种来源的样式表,行内样式元素及html中的可视化属性(例如bgcolor),可视化属性转化为css样式属性。
样式表来源于浏览器默认样式表,及页面作者和用户提供的样式表——有些样式是浏览器用户提供的(浏览器允许用户定义喜欢的样式,例如,在Firefox中,可以通过在Firefox Profile目录下放置样式表实现)。
计算样式的一些困难:
- 样式数据是非常大的结构,保存大量的样式属性会带来内存问题
- 如果不进行优化,找到每个元素匹配的规则会导致性能问题,为每个元素查找匹配的规则都需要遍历整个规则表,这个过程有很大的工作量。选择符可能有复杂的结构,匹配过程如果沿着一条开始看似正确,后来却被证明是无用的路径,则必须去尝试另一条路径。
例如,下面这个复杂选择符
div div div div{…}
这意味着规则应用到三个div的后代div元素,选择树上一条特定的路径去检查,这可能需要遍历节点树,最后却发现它只是两个div的后代,并不使用该规则,然后则需要沿着另一条路径去尝试 - 应用规则涉及非常复杂的级联,它们定义了规则的层次
我们来看一下浏览器如何处理这些问题:
共享样式数据
webkit节点引用样式对象(渲染样式),某些情况下,这些对象可以被节点间共享,这些节点需要是兄弟或是表兄弟节点,并且:
- 这些元素必须处于相同的鼠标状态(比如不能一个处于hover,而另一个不是)
- 不能有元素具有id
- 标签名必须匹配
- class属性必须匹配
- 对应的属性必须相同
- 链接状态必须匹配
- 焦点状态必须匹配
- 不能有元素被属性选择器影响
- 元素不能有行内样式属性
- 不能有生效的兄弟选择器,webcore在任何兄弟选择器相遇时只是简单的抛出一个全局转换,并且在它们显示时使整个文档的样式共享失效,这些包括+选择器和类似:first-child和:last-child这样的选择器。
Firefox规则树 Firefox rule tree
Firefox用两个树用来简化样式计算-规则树和样式上下文树,webkit也有样式对象,但它们并没有存储在类似样式上下文树这样的树中,只是由Dom节点指向其相关的样式。
Firefox样式上下文树:
样式上下文包含最终值,这些值是通过以正确顺序应用所有匹配的规则,并将它们由逻辑值转换为具体的值,例如,如果逻辑值为屏幕的百分比,则通过计算将其转化为绝对单位。样式树的使用确实很巧妙,它使得在节点中共享的这些值不需要被多次计算,同时也节省了存储空间。
所有匹配的规则都存储在规则树中,一条路径中的底层节点拥有最高的优先级,这棵树包含了所找到的所有规则匹配的路径(译注:可以取巧理解为每条路径对应一个节点,路径上包含了该节点所匹配的所有规则)。规则树并不是一开始就为所有节点进行计算,而是在某个节点需要计算样式时,才进行相应的计算并将计算后的路径添加到树中。
我们将树上的路径看成辞典中的单词,假如已经计算出了如下的规则树:
10、资料:
- 浏览器组成及工作原理深度了解
- 前端技术演进(一):Web前端技术基础