<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ 浏览器 - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ freeCodeCamp 是一个免费学习编程的开发者社区，涵盖 Python、HTML、CSS、React、Vue、BootStrap、JSON 教程等，还有活跃的技术论坛和丰富的社区活动，在你学习编程和找工作时为你提供建议和帮助。 ]]>
        </description>
        <link>https://www.freecodecamp.org/chinese/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ 浏览器 - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/chinese/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sat, 30 May 2026 13:56:39 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/chinese/news/tag/browsers/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ 从零开始实现一个玩具版浏览器渲染引擎 ]]>
                </title>
                <description>
                    <![CDATA[ 浏览器渲染原理作为前端必须要了解的知识点之一，在面试中经常会被问到。在一些前端书籍或者培训课程里也会经常被提及，比如 MDN 文档中就有渲染原理 [https://developer.mozilla.org/zh-CN/docs/Web/Performance/How_browsers_work#%E6%B8%B2%E6%9F%93] 的相关描述。 作为一名工作多年的前端，我对于渲染原理自然也是了解的，但是对于它的理解只停留在理论知识层面。所以我决定自己动手实现一个玩具版的渲染引擎。 渲染引擎是浏览器的一部分，它负责将网页内容（HTML、CSS、JavaScript 等）转化为用户可阅读、观看、听到的形式。但是要独自实现一个完整的渲染引擎工作量实在太大了，而且也很困难。于是我决定退一步，打算实现一个玩具版的渲染引擎。刚好 Github 上有一个开源的用 Rust 写的玩具版渲染引擎 robinson [https://github.com/woai3c/tiny-rendering-engine]，于是决定模仿其源码自己用 JavaScript 实现一遍，并且也在 Github 上 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/browser-rendering-engine/</link>
                <guid isPermaLink="false">64fed78058a38a1950d360df</guid>
                
                    <category>
                        <![CDATA[ 浏览器 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ HTML ]]>
                    </category>
                
                    <category>
                        <![CDATA[ CSS ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ woai3c ]]>
                </dc:creator>
                <pubDate>Tue, 12 Sep 2023 03:35:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2023/09/1694616133719.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>浏览器渲染原理作为前端必须要了解的知识点之一，在面试中经常会被问到。在一些前端书籍或者培训课程里也会经常被提及，比如 MDN 文档中就有<a href="https://developer.mozilla.org/zh-CN/docs/Web/Performance/How_browsers_work#%E6%B8%B2%E6%9F%93">渲染原理</a>的相关描述。</p><p>作为一名工作多年的前端，我对于渲染原理自然也是了解的，但是对于它的理解只停留在理论知识层面。所以我决定自己动手实现一个玩具版的渲染引擎。</p><p>渲染引擎是浏览器的一部分，它负责将网页内容（HTML、CSS、JavaScript 等）转化为用户可阅读、观看、听到的形式。但是要独自实现一个完整的渲染引擎工作量实在太大了，而且也很困难。于是我决定退一步，打算实现一个玩具版的渲染引擎。刚好 Github 上有一个开源的用 Rust 写的玩具版渲染引擎 <a href="https://github.com/woai3c/tiny-rendering-engine">robinson</a>，于是决定模仿其源码自己用 JavaScript 实现一遍，并且也在 Github 上开源了<a href="https://github.com/woai3c/tiny-rendering-engine">从零开始实现一个玩具版浏览器渲染引擎</a>。</p><p>这个玩具版的渲染引擎一共分为五个阶段：</p><figure class="kg-card kg-image-card"><img src="https://pic1.zhimg.com/80/v2-266e8e94510504d11b930612229012ac_1440w.webp" class="kg-image" alt="v2-266e8e94510504d11b930612229012ac_1440w" width="600" height="400" loading="lazy"></figure><p>分别是：</p><ol><li>解析 HTML，生成 DOM 树</li><li>解析 CSS，生成 CSS 规则集合</li><li>生成 Style 树</li><li>生成布局树</li><li>绘制</li></ol><p>每个阶段的代码我在仓库上都用一个分支来表示。由于直接看整个渲染引擎的代码可能会比较困难，所以我建议大家从第一个分支开始进行学习，从易到难，这样学习效果更好。</p><figure class="kg-card kg-image-card"><img src="https://pic3.zhimg.com/80/v2-27a55d7173240190324c2f9d18b02a32_1440w.webp" class="kg-image" alt="v2-27a55d7173240190324c2f9d18b02a32_1440w" width="600" height="400" loading="lazy"></figure><p>现在我们先看一下如何编写一个 HTML 解析器。</p><h3 id="html-">HTML 解析器</h3><p>HTML 解析器的作用就是将一连串的 HTML 文本解析为 DOM 树。比如将这样的 HTML 文本：</p><pre><code class="language-html">&lt;div class="lightblue test" id=" div " data-index="1"&gt;test!&lt;/div&gt;</code></pre><p>解析为一个 DOM 树：</p><pre><code class="language-js">{
    "tagName": "div",
    "attributes": {
        "class": "lightblue test",
        "id": "div",
        "data-index": "1"
    },
    "children": [
        {
            "nodeValue": "test!",
            "nodeType": 3
        }
    ],
    "nodeType": 1
}
</code></pre><p>写解析器需要懂一些编译原理的知识，比如词法分析、语法分析什么的。但是我们的玩具版解析器非常简单，即使不懂也没有关系，大家看源码就能明白了。</p><p>再回到上面的那段 HTML 文本，它的整个解析过程可以用下面的图来表示，每一段 HTML 文本都有对应的方法去解析。</p><figure class="kg-card kg-image-card"><img src="https://pic4.zhimg.com/80/v2-19271cbd4bdf074f13f0351d39415b4f_1440w.webp" class="kg-image" alt="v2-19271cbd4bdf074f13f0351d39415b4f_1440w" width="600" height="400" loading="lazy"></figure><p>为了让解析器实现起来简单一点，我们需要对 HTML 的功能进行约束：</p><ol><li>标签必须要成对出现：<code>&lt;div&gt;...&lt;/div&gt;</code></li><li>HTML 属性值必须要有引号包起来 <code>&lt;div class="test"&gt;...&lt;/div&gt;</code></li><li>不支持注释</li><li>尽量不做错误处理</li><li>只支持两种类型节点 <code>Element</code> 和 <code>Text</code></li></ol><p>对解析器的功能进行约束后，代码实现就变得简单多了，现在让我们继续吧。</p><h3 id="-">节点类型</h3><p>首先，为这两种节点 <code>Element</code> 和 <code>Text</code> 定一个适当的数据结构：</p><pre><code class="language-ts">export enum NodeType {
    Element = 1,
    Text = 3,
}

export interface Element {
    tagName: string
    attributes: Record&lt;string, string&gt;
    children: Node[]
    nodeType: NodeType.Element
}

interface Text {
    nodeValue: string
    nodeType: NodeType.Text
}

export type Node = Element | Text
</code></pre><p>然后为这两种节点各写一个生成函数：</p><pre><code class="language-ts">export function element(tagName: string) {
    return {
        tagName,
        attributes: {},
        children: [],
        nodeType: NodeType.Element,
    } as Element
}

export function text(data: string) {
    return {
        nodeValue: data,
        nodeType: NodeType.Text,
    } as Text
}
</code></pre><p>这两个函数在解析到元素节点或者文本节点时调用，调用后会返回对应的 DOM 节点。</p><h3 id="html--1">HTML 解析器的执行过程</h3><p>下面这张图就是整个 HTML 解析器的执行过程：</p><figure class="kg-card kg-image-card"><img src="https://pic3.zhimg.com/80/v2-5fc6cad8bfec2129f8f1c82cb71175a2_1440w.webp" class="kg-image" alt="v2-5fc6cad8bfec2129f8f1c82cb71175a2_1440w" width="600" height="400" loading="lazy"></figure><p>HTML 解析器的入口方法为 <code>parse()</code>，从这开始执行直到遍历完所有 HTML 文本为止：</p><ol><li>判断当前字符是否为 <code>&lt;</code>，如果是，则当作元素节点来解析，调用 <code>parseElement()</code>，否则调用 <code>parseText()</code></li><li><code>parseText()</code> 比较简单，一直往前遍历字符串，直至遇到 <code>&lt;</code> 字符为止。然后将之前遍历过的所有字符当作 <code>Text</code> 节点的值。</li><li><code>parseElement()</code> 则相对复杂一点，它首先要解析出当前的元素标签名称，这段文本用 <code>parseTag()</code> 来解析。</li><li>然后再进入 <code>parseAttrs()</code> 方法，判断是否有属性节点，如果该节点有 <code>class</code> 或者其他 HTML 属性，则会调用 <code>parseAttr()</code> 把 HTML 属性或者 <code>class</code> 解析出来。</li><li>至此，整个元素节点的前半段已经解析完了。接下来需要解析它的子节点。这时就会进入无限递归循环回到第一步，继续解析元素节点或文本节点。</li><li>当所有子节点解析完后，需要调用 <code>parseTag()</code>，看看结束标签名和元素节点的开始标签名是否相同，如果相同，则 <code>parseElement()</code> 或者 <code>parse()</code> 结束，否则报错。</li></ol><h3 id="html--2">HTML 解析器各个方法详解</h3><h3 id="-parse-">入口方法 parse()</h3><p>HTML 的入口方法是 <code>parse(rawText)</code></p><pre><code class="language-ts">parse(rawText: string) {
    this.rawText = rawText.trim()
    this.len = this.rawText.length
    this.index = 0
    this.stack = []

    const root = element('root')
    while (this.index &lt; this.len) {
        this.removeSpaces()
        if (this.rawText[this.index].startsWith('&lt;')) {
            this.index++
            this.parseElement(root)
        } else {
            this.parseText(root)
        }
    }
}
</code></pre><p>入口方法需要遍历所有文本，在一开始它需要判断当前字符是否是 <code>&lt;</code>，如果是，则将它当作元素节点来解析，调用 <code>parseElement()</code>，否则将当前字符作为文本来解析，调用 <code>parseText()</code>。</p><h3 id="-parseelement-">解析元素节点 <code>parseElement()</code></h3><pre><code class="language-ts">private parseElement(parent: Element) {
 // 解析标签
    const tag = this.parseTag()
    // 生成元素节点
    const ele = element(tag)

    this.stack.push(tag)

    parent.children.push(ele)
    // 解析属性
    this.parseAttrs(ele)

    while (this.index &lt; this.len) {
        this.removeSpaces()
        if (this.rawText[this.index].startsWith('&lt;')) {
            this.index++
            this.removeSpaces()
            // 判断是否是结束标签
            if (this.rawText[this.index].startsWith('/')) {
                this.index++
                const startTag = this.stack[this.stack.length - 1]
                // 结束标签
                const endTag = this.parseTag()
                if (startTag !== endTag) {
                    throw Error(`The end tagName ${endTag} does not match start tagName ${startTag}`)
                }

                this.stack.pop()
                while (this.index &lt; this.len &amp;&amp; this.rawText[this.index] !== '&gt;') {
                    this.index++
                }

                break
            } else {
                this.parseElement(ele)
            }
        } else {
            this.parseText(ele)
        }
    }

    this.index++
}
</code></pre><p><code>parseElement()</code> 会依次调用 <code>parseTag()</code> <code>parseAttrs()</code> 解析标签和属性，然后再递归解析子节点，终止条件是遍历完所有的 HTML 文本。</p><h3 id="-parsetext-">解析文本节点 <code>parseText()</code></h3><pre><code class="language-ts">private parseText(parent: Element) {
    let str = ''
    while (
        this.index &lt; this.len
        &amp;&amp; !(this.rawText[this.index] === '&lt;' &amp;&amp; /\w|\//.test(this.rawText[this.index + 1]))
    ) {
        str += this.rawText[this.index]
        this.index++
    }

    this.sliceText()
    parent.children.push(text(removeExtraSpaces(str)))
}
</code></pre><p>解析文本相对简单一点，它会一直往前遍历，直至遇到 <code>&lt;</code> 为止。比如这段文本 <code>&lt;div&gt;test!&lt;/div&gt;</code>，经过 <code>parseText()</code> 解析后拿到的文本是 <code>test!</code>。</p><h3 id="-parsetag-">解析标签 <code>parseTag()</code></h3><p>在进入 <code>parseElement()</code> 后，首先调用就是 <code>parseTag()</code>，它的作用是解析标签名：</p><pre><code class="language-ts">private parseTag() {
    let tag = ''

    this.removeSpaces()

    // get tag name
    while (this.index &lt; this.len &amp;&amp; this.rawText[this.index] !== ' ' &amp;&amp; this.rawText[this.index] !== '&gt;') {
        tag += this.rawText[this.index]
        this.index++
    }

    this.sliceText()
    return tag
}
</code></pre><p>比如这段文本 <code>&lt;div&gt;test!&lt;/div&gt;</code>，经过 <code>parseTag()</code> 解析后拿到的标签名是 <code>div</code>。</p><h3 id="-parseattrs-">解析属性节点 <code>parseAttrs()</code></h3><p>解析完标签名后，接着再解析属性节点：</p><pre><code class="language-ts">// 解析元素节点的所有属性
private parseAttrs(ele: Element) {
 // 一直遍历文本，直至遇到 '&gt;' 字符为止，代表 &lt;div ....&gt; 这一段文本已经解析完了
    while (this.index &lt; this.len &amp;&amp; this.rawText[this.index] !== '&gt;') {
        this.removeSpaces()
        this.parseAttr(ele)
        this.removeSpaces()
    }

    this.index++
}

// 解析单个属性，例如 class="foo bar"
private parseAttr(ele: Element) {
    let attr = ''
    let value = ''
    while (this.index &lt; this.len &amp;&amp; this.rawText[this.index] !== '=' &amp;&amp; this.rawText[this.index] !== '&gt;') {
        attr += this.rawText[this.index++]
    }

    this.sliceText()
    attr = attr.trim()
    if (!attr.trim()) return

    this.index++
    let startSymbol = ''
    if (this.rawText[this.index] === "'" || this.rawText[this.index] === '"') {
        startSymbol = this.rawText[this.index++]
    }

    while (this.index &lt; this.len &amp;&amp; this.rawText[this.index] !== startSymbol) {
        value += this.rawText[this.index++]
    }

    this.index++
    ele.attributes[attr] = value.trim()
    this.sliceText()
}
</code></pre><p><code>parseAttr()</code> 可以将这样的文本 <code>class="test"</code> 解析为一个对象 <code>{ class: "test" }</code>。</p><h3 id="--1">其他辅助方法</h3><p>有时不同的节点、属性之间有很多多余的空格，所以需要写一个方法将多余的空格清除掉。</p><pre><code class="language-ts">protected removeSpaces() {
    while (this.index &lt; this.len &amp;&amp; (this.rawText[this.index] === ' ' || this.rawText[this.index] === '\n')) {
        this.index++
    }

    this.sliceText()
}
</code></pre><p>同时为了方便调试，开发者经常需要打断点看当前正在遍历的字符是什么。如果以前遍历过的字符串还在，那么是比较难调试的，因为开发者需要根据 index 的值自己去找当前遍历的字符是什么。所以所有解析完的 HTML 文本，都需要截取掉，确保当前的 HTML 文本都是没有被遍历：</p><pre><code class="language-ts">protected sliceText() {
    this.rawText = this.rawText.slice(this.index)
    this.len = this.rawText.length
    this.index = 0
}
</code></pre><p><code>sliceText()</code> 方法的作用就是截取已经遍历过的 HTML 文本。用下图来做例子，假设当前要解析 <code>div</code> 这个标签名：</p><figure class="kg-card kg-image-card"><img src="https://pic2.zhimg.com/80/v2-d18d74c75c7c028b686132e9c2377b9d_1440w.webp" class="kg-image" alt="v2-d18d74c75c7c028b686132e9c2377b9d_1440w" width="1169" height="301" loading="lazy"></figure><p>那么解析后需要对 HTML 文本进行截取，就像下图这样：</p><figure class="kg-card kg-image-card"><img src="https://pic2.zhimg.com/80/v2-8a073dda6507fcb4e834339399065ba5_1440w.webp" class="kg-image" alt="v2-8a073dda6507fcb4e834339399065ba5_1440w" width="923" height="179" loading="lazy"></figure><h3 id="--2">小结</h3><p>至此，整个 HTML 解析器的逻辑已经讲完了，所有代码加起来 200 行左右，如果不算 TS 各种类型声明，代码只有 100 多行。</p><h3 id="css-">CSS 解析器</h3><p>CSS 样式表是一系列的 CSS 规则集合，而 CSS 解析器的作用就是将 CSS 文本解析为 CSS 规则集合。</p><pre><code class="language-css">div, p {
    font-size: 88px;
    color: #000;
}</code></pre><p>例如上面的 CSS 文本，经过解析器解析后，会生成下面的 CSS 规则集合：</p><pre><code class="language-js">[
    {
        "selectors": [
            {
                "id": "",
                "class": "",
                "tagName": "div"
            },
            {
                "id": "",
                "class": "",
                "tagName": "p"
            }
        ],
        "declarations": [
            {
                "name": "font-size",
                "value": "88px"
            },
            {
                "name": "color",
                "value": "#000"
            }
        ]
    }
]
</code></pre><p>每个规则都有一个 <code>selector</code> 和 <code>declarations</code> 属性，其中 <code>selectors</code> 表示 CSS 选择器，<code>declarations</code> 表示 CSS 的属性描述集合。</p><pre><code class="language-ts">export interface Rule {
    selectors: Selector[]
    declarations: Declaration[]
}

export interface Selector {
    tagName: string
    id: string
    class: string
}

export interface Declaration {
    name: string
    value: string | number
}
</code></pre><figure class="kg-card kg-image-card"><img src="https://pic4.zhimg.com/80/v2-4ae9e0e860a5248e0e4e3a82f2c983eb_1440w.webp" class="kg-image" alt="v2-4ae9e0e860a5248e0e4e3a82f2c983eb_1440w" width="758" height="267" loading="lazy"></figure><p>每一条 CSS 规则都可以包含多个选择器和多个 CSS 属性。</p><h3 id="-css-parserule-">解析 CSS 规则 <code>parseRule()</code></h3><pre><code class="language-ts">private parseRule() {
    const rule: Rule = {
        selectors: [],
        declarations: [],
    }

    rule.selectors = this.parseSelectors()
    rule.declarations = this.parseDeclarations()

    return rule
}
</code></pre><p>在 <code>parseRule()</code> 里，它分别调用了 <code>parseSelectors()</code> 去解析 CSS 选择器，然后再对剩余的 CSS 文本执行 <code>parseDeclarations()</code> 去解析 CSS 属性。</p><h3 id="-parseselector-">解析选择器 <code>parseSelector()</code></h3><pre><code class="language-ts">private parseSelector() {
    const selector: Selector = {
        id: '',
        class: '',
        tagName: '',
    }

    switch (this.rawText[this.index]) {
        case '.':
            this.index++
            selector.class = this.parseIdentifier()
            break
        case '#':
            this.index++
            selector.id = this.parseIdentifier()
            break
        case '*':
            this.index++
            selector.tagName = '*'
            break
        default:
            selector.tagName = this.parseIdentifier()
    }

    return selector
}

private parseIdentifier() {
    let result = ''
    while (this.index &lt; this.len &amp;&amp; this.identifierRE.test(this.rawText[this.index])) {
        result += this.rawText[this.index++]
    }

    this.sliceText()
    return result
}
</code></pre><p>选择器我们只支持标签名称、前缀为 <code>#</code> 的 ID 、前缀为任意数量的类名 <code>.</code> 或上述的某种组合。如果标签名称为 <code>*</code>，则表示它是一个通用选择器，可以匹配任何标签。</p><p>标准的 CSS 解析器在遇到无法识别的部分时，会将它丢掉，然后继续解析其余部分。主要是为了兼容旧浏览器和防止发生错误导致程序中断。我们的 CSS 解析器为了实现简单，没有做这方面的做错误处理。</p><h3 id="-css-parsedeclaration-">解析 CSS 属性 <code>parseDeclaration()</code></h3><pre><code class="language-ts">private parseDeclaration() {
    const declaration: Declaration = { name: '', value: '' }
    this.removeSpaces()
    declaration.name = this.parseIdentifier()
    this.removeSpaces()

    while (this.index &lt; this.len &amp;&amp; this.rawText[this.index] !== ':') {
        this.index++
    }

    this.index++ // clear :
    this.removeSpaces()
    declaration.value = this.parseValue()
    this.removeSpaces()

    return declaration
}
</code></pre><p><code>parseDeclaration()</code> 会将 <code>color: red;</code> 解析为一个对象 <code>{ name: "color", value: "red" }</code>。</p><h3 id="--3">小结</h3><p>CSS 解析器相对来说简单多了，因为很多知识点在 HTML 解析器中已经讲到。整个 CSS 解析器的代码大概 100 多行，如果你阅读过 HTML 解析器的源码，相信看 CSS 解析器的源码会更轻松。</p><h3 id="--4">构建样式树</h3><p>本阶段的目标是写一个样式构建器，输入 DOM 树和 CSS 规则集合，生成一棵样式树 Style tree。</p><figure class="kg-card kg-image-card"><img src="https://pic1.zhimg.com/80/v2-266e8e94510504d11b930612229012ac_1440w.webp" class="kg-image" alt="v2-266e8e94510504d11b930612229012ac_1440w" width="739" height="204" loading="lazy"></figure><p>样式树的每一个节点都包含了 CSS 属性值以及它对应的 DOM 节点引用：</p><pre><code class="language-ts">interface AnyObject {
    [key: string]: any
}

export interface StyleNode {
    node: Node // DOM 节点
    values: AnyObject // style 属性值
    children: StyleNode[] // style 子节点
}
</code></pre><p>先来看一个简单的示例：</p><pre><code class="language-html">&lt;div&gt;test&lt;/div&gt;
div {
    font-size: 88px;
    color: #000;
}</code></pre><p>上述的 HTML、CSS 文本在经过样式树构建器处理后生成的样式树如下：</p><pre><code class="language-js">{
    "node": { // DOM 节点
        "tagName": "div",
        "attributes": {},
        "children": [
            {
                "nodeValue": "test",
                "nodeType": 3
            }
        ],
        "nodeType": 1
    },
    "values": { // CSS 属性值
        "font-size": "88px",
        "color": "#000"
    },
    "children": [ // style tree 子节点
        {
            "node": {
                "nodeValue": "test",
                "nodeType": 3
            },
            "values": { // text 节点继承了父节点样式
                "font-size": "88px",
                "color": "#000"
            },
            "children": []
        }
    ]
}
</code></pre><h3 id="-dom-">遍历 DOM 树</h3><p>现在我们需要遍历 DOM 树。对于 DOM 树中的每个节点，我们都要在样式树中查找是否有匹配的 CSS 规则。</p><pre><code class="language-ts">export function getStyleTree(eles: Node | Node[], cssRules: Rule[], parent?: StyleNode) {
    if (Array.isArray(eles)) {
        return eles.map((ele) =&gt; getStyleNode(ele, cssRules, parent))
    }

    return getStyleNode(eles, cssRules, parent)
}
</code></pre><h3 id="--5">匹配选择器</h3><p>匹配选择器实现起来非常容易，因为我们的CSS 解析器仅支持简单的选择器。 只需要查看元素本身即可判断选择器是否与元素匹配。</p><pre><code class="language-ts">/**
 * css 选择器是否匹配元素
 */
function isMatch(ele: Element, selectors: Selector[]) {
    return selectors.some((selector) =&gt; {
        // 通配符
        if (selector.tagName === '*') return true
        if (selector.tagName === ele.tagName) return true
        if (ele.attributes.id === selector.id) return true

        if (ele.attributes.class) {
            const classes = ele.attributes.class.split(' ').filter(Boolean)
            const classes2 = selector.class.split(' ').filter(Boolean)
            for (const name of classes) {
                if (classes2.includes(name)) return true
            }
        }

        return false
    })
}
</code></pre><p>当查找到匹配的 DOM 节点后，再将 DOM 节点和它匹配的 CSS 属性组合在一起，生成样式树节点 styleNode：</p><pre><code class="language-ts">function getStyleNode(ele: Node, cssRules: Rule[], parent?: StyleNode) {
    const styleNode: StyleNode = {
        node: ele,
        values: getStyleValues(ele, cssRules, parent),
        children: [],
    }

    if (ele.nodeType === NodeType.Element) {
        // 合并内联样式
        if (ele.attributes.style) {
            styleNode.values = { ...styleNode.values, ...getInlineStyle(ele.attributes.style) }
        }

        styleNode.children = ele.children.map((e) =&gt; getStyleNode(e, cssRules, styleNode)) as unknown as StyleNode[]
    }

    return styleNode
}

function getStyleValues(ele: Node, cssRules: Rule[], parent?: StyleNode) {
    const inheritableAttrValue = getInheritableAttrValues(parent)

    // 文本节点继承父元素的可继承属性
    if (ele.nodeType === NodeType.Text) return inheritableAttrValue

    return cssRules.reduce((result: AnyObject, rule) =&gt; {
        if (isMatch(ele as Element, rule.selectors)) {
            result = { ...result, ...cssValueArrToObject(rule.declarations) }
        }

        return result
    }, inheritableAttrValue)
}
</code></pre><p>在 CSS 选择器中，不同的选择器优先级是不同的，比如 id 选择器就比类选择器的优先级要高。但是我们这里没有实现选择器优先级，为了实现简单，所有的选择器优先级是一样的。</p><h3 id="--6">继承属性</h3><p>文本节点无法匹配选择器，那它的样式从哪来？答案就是继承，它可以继承父节点的样式。</p><p>在 CSS 中存在很多继承属性，即使子元素没有声明这些属性，也可以从父节点里继承。比如字体颜色、字体家族等属性，都是可以被继承的。为了实现简单，这里只支持继承父节点的 <code>color</code>、<code>font-size</code> 属性。</p><pre><code class="language-ts">// 子元素可继承的属性，这里只写了两个，实际上还有很多
const inheritableAttrs = ['color', 'font-size']

/**
 * 获取父元素可继承的属性值
 */
function getInheritableAttrValues(parent?: StyleNode) {
    if (!parent) return {}
    const keys = Object.keys(parent.values)
    return keys.reduce((result: AnyObject, key) =&gt; {
        if (inheritableAttrs.includes(key)) {
            result[key] = parent.values[key]
        }

        return result
    }, {})
}
</code></pre><h3 id="--7">内联样式</h3><p>在 CSS 中，内联样式的优先级是除了 <code>!important</code> 之外最高的。</p><pre><code class="language-html">&lt;span style="color: red; background: yellow;"&gt;</code></pre><p>我们可以在调用 <code>getStyleValues()</code> 函数获得当前 DOM 节点的 CSS 属性值后，再去取当前节点的内联样式值。并对当前 DOM 节点的 CSS 样式值进行覆盖。</p><pre><code class="language-ts">styleNode.values = { ...styleNode.values, ...getInlineStyle(ele.attributes.style) }

function getInlineStyle(str: string) {
    str = str.trim()
    if (!str) return {}
    const arr = str.split(';')
    if (!arr.length) return {}

    return arr.reduce((result: AnyObject, item: string) =&gt; {
        const data = item.split(':')
        if (data.length === 2) {
            result[data[0].trim()] = data[1].trim()
        }

        return result
    }, {})
}
</code></pre><h3 id="--8">布局树</h3><p>第四阶段讲的是如何将样式树转化为布局树，也是整个渲染引擎相对比较复杂的部分。</p><figure class="kg-card kg-image-card"><img src="https://pic1.zhimg.com/80/v2-266e8e94510504d11b930612229012ac_1440w.webp" class="kg-image" alt="v2-266e8e94510504d11b930612229012ac_1440w" width="739" height="204" loading="lazy"></figure><h3 id="css--1">CSS 盒子模型</h3><p>在 CSS 中，所有的 DOM 节点都可以当作一个盒子。这个盒子模型包含了内容、内边距、边框、外边距以及在页面中的位置信息。</p><figure class="kg-card kg-image-card"><img src="https://pic3.zhimg.com/80/v2-6ca1a4d0cb14b09aea28fefe3dc91dc2_1440w.webp" class="kg-image" alt="v2-6ca1a4d0cb14b09aea28fefe3dc91dc2_1440w" width="494" height="341" loading="lazy"></figure><p>我们可以用以下的数据结构来表示盒子模型：</p><pre><code class="language-ts">export default class Dimensions {
    content: Rect
    padding: EdgeSizes
    border: EdgeSizes
    margin: EdgeSizes
}

export default class Rect {
    x: number
    y: number
    width: number
    height: number
}

export interface EdgeSizes {
    top: number
    right: number
    bottom: number
    left: number
}
</code></pre><h3 id="--9">块布局和内联布局</h3><p>CSS 的 <code>display</code> 属性决定了盒子在页面中的布局方式。<code>display</code> 的类型有很多种，例如 <code>block</code>、<code>inline</code>、<code>flex</code> 等等，但这里只支持 <code>block</code> 和 <code>inline</code> 两种布局方式，并且所有盒子的默认布局方式为 <code>display: inline</code>。</p><p>我会用伪 HTML 代码来描述它们之间的区别：</p><pre><code class="language-html">&lt;container&gt;
  &lt;a&gt;&lt;/a&gt;
  &lt;b&gt;&lt;/b&gt;
  &lt;c&gt;&lt;/c&gt;
  &lt;d&gt;&lt;/d&gt;
&lt;/container&gt;</code></pre><p>块布局会将盒子从上至下的垂直排列。</p><figure class="kg-card kg-image-card"><img src="https://pic3.zhimg.com/80/v2-02572058706b774cb4eb6d2920127b6a_1440w.webp" class="kg-image" alt="v2-02572058706b774cb4eb6d2920127b6a_1440w" width="384" height="224" loading="lazy"></figure><p>内联布局则会将盒子从左至右的水平排列。</p><figure class="kg-card kg-image-card"><img src="https://pic3.zhimg.com/80/v2-dbd175cba44b114a7a8fe74efac00b7a_1440w.webp" class="kg-image" alt="v2-dbd175cba44b114a7a8fe74efac00b7a_1440w" width="393" height="130" loading="lazy"></figure><p>如果容器内同时存在块布局和内联布局，则会用一个匿名布局将内联布局包裹起来。</p><figure class="kg-card kg-image-card"><img src="https://pic3.zhimg.com/80/v2-98271cb522c63a126d86abfd9648ed8e_1440w.webp" class="kg-image" alt="v2-98271cb522c63a126d86abfd9648ed8e_1440w" width="388" height="236" loading="lazy"></figure><p>这样就能将内联布局的盒子和其他块布局的盒子区别开来。</p><p>通常情况下内容是垂直增长的。也就是说，在容器中添加子节点通常会使容器更高，而不是更宽。另一种说法是，默认情况下，子节点的宽度取决于其容器的宽度，而容器的高度取决于其子节点的高度。</p><h3 id="--10">布局树</h3><p>布局树是所有盒子节点的集合。</p><pre><code class="language-ts">export default class LayoutBox {
 dimensions: Dimensions
 boxType: BoxType
 children: LayoutBox[]
 styleNode: StyleNode
}
</code></pre><p>盒子节点的类型可以是 <code>block</code>、<code>inilne</code> 和 <code>anonymous</code>。</p><pre><code class="language-ts">export enum BoxType {
    BlockNode = 'BlockNode',
    InlineNode = 'InlineNode',
    AnonymousBlock = 'AnonymousBlock',
}
</code></pre><p>我们构建样式树时，需要根据每一个 DOM 节点的 <code>display</code> 属性来生成对应的盒子节点。</p><pre><code class="language-ts">export function getDisplayValue(styleNode: StyleNode) {
    return styleNode.values?.display ?? Display.Inline
}
</code></pre><p>如果 DOM 节点 <code>display</code> 属性的值为 <code>none</code>，则在构建布局树的过程中，无需将这个 DOM 节点添加到布局树上，直接忽略它就可以了。</p><p>如果一个块节点包含一个内联子节点，则需要创建一个匿名块（实际上就是块节点）来包含它。如果一行中有多个子节点，则将它们全部放在同一个匿名容器中。</p><pre><code class="language-ts">function buildLayoutTree(styleNode: StyleNode) {
    if (getDisplayValue(styleNode) === Display.None) {
        throw new Error('Root node has display: none.')
    }

    const layoutBox = new LayoutBox(styleNode)

    let anonymousBlock: LayoutBox | undefined
    for (const child of styleNode.children) {
        const childDisplay = getDisplayValue(child)
        // 如果 DOM 节点 display 属性值为 none，直接跳过
        if (childDisplay === Display.None) continue

        if (childDisplay === Display.Block) {
            anonymousBlock = undefined
            layoutBox.children.push(buildLayoutTree(child))
        } else {
         // 创建一个匿名容器，用于容纳内联节点
            if (!anonymousBlock) {
                anonymousBlock = new LayoutBox()
                layoutBox.children.push(anonymousBlock)
            }

            anonymousBlock.children.push(buildLayoutTree(child))
        }
    }

    return layoutBox
}
</code></pre><h3 id="--11">遍历布局树</h3><p>现在开始构建布局树，入口函数是 <code>getLayoutTree()</code>：</p><pre><code class="language-ts">export function getLayoutTree(styleNode: StyleNode, parentBlock: Dimensions) {
    parentBlock.content.height = 0
    const root = buildLayoutTree(styleNode)
    root.layout(parentBlock)
    return root
}
</code></pre><p>它将遍历样式树，利用样式树节点提供的相关信息，生成一个 <code>LayoutBox</code> 对象，然后调用 <code>layout()</code> 方法。计算每个盒子节点的位置、尺寸信息。</p><p>在本节内容的开头有提到过，盒子的宽度取决于其父节点，而高度取决于子节点。这意味着，我们的代码在计算宽度时需要自上而下遍历树，这样它就可以在知道父节点的宽度后设置子节点的宽度。然后自下而上遍历以计算高度，这样父节点的高度就可以在计算子节点的相关信息后进行计算。</p><pre><code class="language-ts">layout(parentBlock: Dimensions) {
 // 子节点的宽度依赖于父节点的宽度，所以要先计算当前节点的宽度，再遍历子节点
    this.calculateBlockWidth(parentBlock)
    // 计算盒子节点的位置
    this.calculateBlockPosition(parentBlock)
    // 遍历子节点并计算对位置、尺寸信息
    this.layoutBlockChildren()
    // 父节点的高度依赖于其子节点的高度，所以计算子节点的高度后，再计算自己的高度
    this.calculateBlockHeight()
}
</code></pre><p>这个方法执行布局树的单次遍历，向下执行宽度计算，向上执行高度计算。一个真正的布局引擎可能会执行几次树遍历，有些是自上而下的，有些是自下而上的。</p><h3 id="--12">计算宽度</h3><p>现在，我们先来计算盒子节点的宽度，这部分比较复杂，需要详细的讲解。</p><p>首先，我们要拿到当前节点的 <code>width</code> <code>padding</code> <code>border</code> <code>margin</code> 等信息：</p><pre><code class="language-ts">calculateBlockWidth(parentBlock: Dimensions) {
 // 初始值
 const styleValues = this.styleNode?.values || {}

 // 初始值为 auto
 let width = styleValues.width ?? 'auto'
 let marginLeft = styleValues['margin-left'] || styleValues.margin || 0
 let marginRight = styleValues['margin-right'] || styleValues.margin || 0

 let borderLeft = styleValues['border-left'] || styleValues.border || 0
 let borderRight = styleValues['border-right'] || styleValues.border || 0

 let paddingLeft = styleValues['padding-left'] || styleValues.padding || 0
 let paddingRight = styleValues['padding-right'] || styleValues.padding || 0

 // 拿到父节点的宽度，如果某个属性为 'auto'，则将它设为 0
 let totalWidth = sum(width, marginLeft, marginRight, borderLeft, borderRight, paddingLeft, paddingRight)
 // ...
</code></pre><p>如果这些属性没有设置，就使用 0 作为默认值。拿到当前节点的总宽度后，还需要和父节点对比一下是否相等。如果宽度或边距设置为 <code>auto</code>，则可以对这两个属性进行适当展开或收缩以适应可用空间。所以现在需要对当前节点的宽度进行检查。</p><pre><code class="language-ts">const isWidthAuto = width === 'auto'
const isMarginLeftAuto = marginLeft === 'auto'
const isMarginRightAuto = marginRight === 'auto'

// 当前块的宽度如果超过了父元素宽度，则将它的可扩展外边距设为 0
if (!isWidthAuto &amp;&amp; totalWidth &gt; parentWidth) {
    if (isMarginLeftAuto) {
        marginLeft = 0
    }

    if (isMarginRightAuto) {
        marginRight = 0
    }
}

// 根据父子元素宽度的差值，去调整当前元素的宽度
const underflow = parentWidth - totalWidth

// 如果三者都有值，则将差值填充到 marginRight
if (!isWidthAuto &amp;&amp; !isMarginLeftAuto &amp;&amp; !isMarginRightAuto) {
    marginRight += underflow
} else if (!isWidthAuto &amp;&amp; !isMarginLeftAuto &amp;&amp; isMarginRightAuto) {
 // 如果右边距是 auto，则将 marginRight 设为差值
    marginRight = underflow
} else if (!isWidthAuto &amp;&amp; isMarginLeftAuto &amp;&amp; !isMarginRightAuto) {
 // 如果左边距是 auto，则将 marginLeft 设为差值
    marginLeft = underflow
} else if (isWidthAuto) {
    // 如果只有 width 是 auto，则将另外两个值设为 0
    if (isMarginLeftAuto) {
        marginLeft = 0
    }

    if (isMarginRightAuto) {
        marginRight = 0
    }

    if (underflow &gt;= 0) {
        // 展开宽度，填充剩余空间，原来的宽度是 auto，作为 0 来计算的
        width = underflow
    } else {
        // 宽度不能为负数，所以需要调整 marginRight 来代替
        width = 0
        // underflow 为负数，相加实际上就是缩小当前节点的宽度
        marginRight += underflow
    }
} else if (!isWidthAuto &amp;&amp; isMarginLeftAuto &amp;&amp; isMarginRightAuto) {
    // 如果只有 marginLeft 和 marginRight 是 auto，则将两者设为 underflow 的一半
    marginLeft = underflow / 2
    marginRight = underflow / 2
}
</code></pre><p>详细的计算过程请看上述代码，重要的地方都已经标上注释了。</p><p>通过对比当前节点和父节点的宽度，我们可以拿到一个差值：</p><pre><code class="language-ts">// 根据父子元素宽度的差值，去调整当前元素的宽度
const underflow = parentWidth - totalWidth
</code></pre><p>如果这个差值为正数，说明子节点宽度小于父节点；如果差值为负数，说明子节点大于父节。上面这段代码逻辑其实就是根据 <code>underflow</code> <code>width</code> <code>padding</code> <code>margin</code> 等值对子节点的宽度、边距进行调整，以适应父节点的宽度。</p><h3 id="--13">定位</h3><p>计算当前节点的位置相对来说简单一点。这个方法会根据当前节点的 <code>margin</code> <code>border</code> <code>padding</code> 样式以及父节点的位置信息对当前节点进行定位：</p><pre><code class="language-ts">calculateBlockPosition(parentBlock: Dimensions) {
 const styleValues = this.styleNode?.values || {}
 const { x, y, height } = parentBlock.content
 const dimensions = this.dimensions

 dimensions.margin.top = transformValueSafe(styleValues['margin-top'] || styleValues.margin || 0)
 dimensions.margin.bottom = transformValueSafe(styleValues['margin-bottom'] || styleValues.margin || 0)

 dimensions.border.top = transformValueSafe(styleValues['border-top'] || styleValues.border || 0)
 dimensions.border.bottom = transformValueSafe(styleValues['border-bottom'] || styleValues.border || 0)

 dimensions.padding.top = transformValueSafe(styleValues['padding-top'] || styleValues.padding || 0)
 dimensions.padding.bottom = transformValueSafe(styleValues['padding-bottom'] || styleValues.padding || 0)

 dimensions.content.x = x + dimensions.margin.left + dimensions.border.left + dimensions.padding.left
 dimensions.content.y = y + height + dimensions.margin.top + dimensions.border.top + dimensions.padding.top
}

function transformValueSafe(val: number | string) {
    if (val === 'auto') return 0
    return parseInt(String(val))
}
</code></pre><p>比如获取当前节点内容区域的 x 坐标，计算方式如下：</p><pre><code class="language-ts">dimensions.content.x = x + dimensions.margin.left + dimensions.border.left + dimensions.padding.left
</code></pre><figure class="kg-card kg-image-card"><img src="https://pic2.zhimg.com/80/v2-23cf320afb0d12b4b25079d4d1a57805_1440w.webp" class="kg-image" alt="v2-23cf320afb0d12b4b25079d4d1a57805_1440w" width="1140" height="603" loading="lazy"></figure><h3 id="--14">遍历子节点</h3><p>在计算高度之前，需要先遍历子节点，因为父节点的高度需要根据它下面子节点的高度进行适配。</p><pre><code class="language-ts">layoutBlockChildren() {
    const { dimensions } = this
    for (const child of this.children) {
        child.layout(dimensions)
        // 遍历子节点后，再计算父节点的高度
        dimensions.content.height += child.dimensions.marginBox().height
    }
}
</code></pre><p>每个节点的高度就是它上下两个外边距之间的差值，所以可以通过 <code>marginBox()</code> 获得高度：</p><pre><code class="language-ts">export default class Dimensions {
    content: Rect
    padding: EdgeSizes
    border: EdgeSizes
    margin: EdgeSizes

    constructor() {
        const initValue = {
            top: 0,
            right: 0,
            bottom: 0,
            left: 0,
        }

        this.content = new Rect()

        this.padding = { ...initValue }
        this.border = { ...initValue }
        this.margin = { ...initValue }
    }

    paddingBox() {
        return this.content.expandedBy(this.padding)
    }

    borderBox() {
        return this.paddingBox().expandedBy(this.border)
    }

    marginBox() {
        return this.borderBox().expandedBy(this.margin)
    }
}
export default class Rect {
    x: number
    y: number
    width: number
    height: number

    constructor() {
        this.x = 0
        this.y = 0
        this.width = 0
        this.height = 0
    }

    expandedBy(edge: EdgeSizes) {
        const rect = new Rect()
        rect.x = this.x - edge.left
        rect.y = this.y - edge.top
        rect.width = this.width + edge.left + edge.right
        rect.height = this.height + edge.top + edge.bottom

        return rect
    }
}
</code></pre><p>遍历子节点并执行完相关计算方法后，再将各个子节点的高度进行相加，得到父节点的高度。</p><h3 id="height-">height 属性</h3><p>默认情况下，节点的高度等于其内容的高度。但如果手动设置了 <code>height</code> 属性，则需要将节点的高度设为指定的高度：</p><pre><code class="language-ts">calculateBlockHeight() {
    // 如果元素设置了 height，则使用 height，否则使用 layoutBlockChildren() 计算出来的高度
    const height = this.styleNode?.values.height
    if (height) {
        this.dimensions.content.height = parseInt(height)
    }
}
</code></pre><p>为了简单起见，我们不需要实现<a href="https://link.zhihu.com/?target=https%3A//developer.mozilla.org/zh-CN/docs/Web/CSS/CSS_box_model/Mastering_margin_collapsing" rel="nofollow noreferrer">外边距折叠</a>。</p><h3 id="--15">小结</h3><p>布局树是渲染引擎最复杂的部分，这一阶段结束后，我们就了解了布局树中每个盒子节点在页面中的具体位置和尺寸信息。下一步，就是如何把布局树渲染到页面上了。</p><h3 id="--16">绘制</h3><p>绘制阶段主要是根据布局树中各个节点的位置、尺寸信息将它们绘制到页面。目前大多数计算机使用光栅（raster，也称为位图）显示技术。将布局树各个节点绘制到页面的这个过程也被称为“光栅化”。</p><p>浏览器通常在图形API和库（如Skia、Cairo、Direct2D等）的帮助下实现光栅化。这些API提供绘制多边形、直线、曲线、渐变和文本的功能。</p><p>实际上绘制才是最难的部分，但是这一步我们有现成的 <a href="https://link.zhihu.com/?target=https%3A//github.com/Automattic/node-canvas" rel="nofollow noreferrer">canvas</a> 库可以用，不用自己实现一个光栅器，所以相对来说就变得简单了。在真正开始绘制阶段之前，我们先来学习一些关于计算机如何绘制图像、文本的基础知识，有助于我们理解光栅化的具体实现过程。</p><h3 id="--17">计算机如何绘制图像、文本</h3><p>在计算机底层进行像素绘制属于硬件操作，它依赖于屏幕和显卡接口的具体细节。为了简单起点，我们可以用一段内存区域来表示屏幕，内存的一个 bit 就代表了屏幕中的一个像素。比如在屏幕中的 <code>(x,y)</code> 坐标绘制一个像素，可以用 <code>memory[x + y * rowSize] = 1</code> 来表示。从屏幕左上角开始，列是从左至右开始计数，行是从上至下开始计数。因此屏幕最左上角的坐标是 <code>(0,0)</code>。</p><p>为了简单起见，我们用 1 bit 来表示屏幕的一个像素，0 代表白色，1 代表黑色。屏幕每一行的长度用变量 <code>rowSzie</code> 表示，每一列的高度用 <code>colSize</code> 表示。</p><figure class="kg-card kg-image-card"><img src="https://pic4.zhimg.com/80/v2-6e4622b6e30abe0a476676e106b8d027_1440w.webp" class="kg-image" alt="v2-6e4622b6e30abe0a476676e106b8d027_1440w" width="496" height="358" loading="lazy"></figure><h3 id="--18">绘制线条</h3><p>如果我们要在计算机上绘制一条直线，那么只要知道计算机的起点坐标 <code>(x1,y1)</code> 和终点坐标 <code>(x2,y2)</code> 就可以了。</p><figure class="kg-card kg-image-card"><img src="https://pic2.zhimg.com/80/v2-4cb7322d209b50961a3979ee8e8e51bd_1440w.webp" class="kg-image" alt="v2-4cb7322d209b50961a3979ee8e8e51bd_1440w" width="400" height="246" loading="lazy"></figure><p>然后根据 <code>memory[x + y * rowSize] = 1</code> 公式，将 <code>(x1,y1)</code> 至 <code>(x2,y2)</code> 之间对应的内存区域置为 1，这样就画出来了一条直线。</p><h3 id="--19">绘制文本</h3><p>为了在屏幕上显示文本，首先必须将物理上基于像素点的屏幕，在逻辑上以字符为单位划分成若干区域，每个区域能输出单个完整的字符。假设有一个 256 行 512 列的屏幕，如果为每个字符分配一个 11*8 像素的网格，那么屏幕上总共能显示 23 行，每行 64 个字符（还有 3 行像素没使用）。</p><p>有了这些前提条件后，我们现在打算在屏幕上画一个 <code>A</code>：</p><figure class="kg-card kg-image-card"><img src="https://pic1.zhimg.com/80/v2-887b0d213e83b3958eb78c87f4b5d644_1440w.webp" class="kg-image" alt="v2-887b0d213e83b3958eb78c87f4b5d644_1440w" width="313" height="362" loading="lazy"></figure><p>上图的 <code>A</code> 在内存区域中用 11*8 像素的网格表示。为了在内存区域中绘制它，我们可以用一个二维数组来表示它：</p><pre><code class="language-js">const charA = [
 [0, 0, 1, 1, 0, 0, 0, 0], // 按从左至右的顺序来读取 bit，转换成十进制数字就是 12
 [0, 1, 1, 1, 1, 0, 0, 0], // 30
 [1, 1, 0, 0, 1, 1, 0, 0], // 51
 [1, 1, 0, 0, 1, 1, 0, 0], // 51
 [1, 1, 1, 1, 1, 1, 0, 0], // 63
 [1, 1, 0, 0, 1, 1, 0, 0], // 51
 [1, 1, 0, 0, 1, 1, 0, 0], // 51
 [1, 1, 0, 0, 1, 1, 0, 0], // 51
 [1, 1, 0, 0, 1, 1, 0, 0], // 51
 [0, 0, 0, 0, 0, 0, 0, 0], // 0
 [0, 0, 0, 0, 0, 0, 0, 0], // 0
]
</code></pre><p>上面二维数组的第一项，代表了第一行内存区域每个 bit 的取值。一共 11 行，画出了一个字母 <code>A</code>。</p><p>如果我们为 26 个字母都建一个映射表，按 ascii 的编码来排序，那么 <code>charsMap[65]</code> 就代表字符 <code>A</code>，当用户在键盘上按下 <code>A</code> 键时，就把 <code>charsMap[65]</code> 对应的数据输出到内存区域上，这样屏幕上就显示了一个字符 <code>A</code>。</p><h3 id="--20">绘制布局树</h3><p>科普完关于绘制屏幕的基础知识后，我们现在正式开始绘制布局树（为了方便，我们使用 <a href="https://link.zhihu.com/?target=https%3A//github.com/Automattic/node-canvas" rel="nofollow noreferrer">node-canvas</a> 库）。</p><p>首先要遍历整个布局树，然后逐个节点进行绘制：</p><pre><code class="language-ts">function renderLayoutBox(layoutBox: LayoutBox, ctx: CanvasRenderingContext2D, parent?: LayoutBox) {
    renderBackground(layoutBox, ctx)
    renderBorder(layoutBox, ctx)
    renderText(layoutBox, ctx, parent)
    for (const child of layoutBox.children) {
        renderLayoutBox(child, ctx, layoutBox)
    }
}
</code></pre><p>这个函数对每个节点依次绘制背景色、边框、文本，然后再递归绘制所有子节点。</p><p>默认情况下，HTML 元素按照它们出现的顺序进行绘制。如果两个元素重叠，则后一个元素将绘制在前一个元素之上。这种排序反映在我们的布局树中，它将按照元素在 DOM 树中出现的顺序绘制元素。</p><h3 id="--21">绘制背景色</h3><pre><code class="language-ts">function renderBackground(layoutBox: LayoutBox, ctx: CanvasRenderingContext2D) {
    const { width, height, x, y } = layoutBox.dimensions.borderBox()
    ctx.fillStyle = getStyleValue(layoutBox, 'background')
    ctx.fillRect(x, y, width, height)
}
</code></pre><p>首先拿到布局节点的位置、尺寸信息，以 <code>x,y</code> 作为起点，绘制矩形区域。并且以 CSS 属性 <code>background</code> 的值作为背景色进行填充。</p><h3 id="--22">绘制边框</h3><pre><code class="language-ts">function renderBorder(layoutBox: LayoutBox, ctx: CanvasRenderingContext2D) {
    const { width, height, x, y } = layoutBox.dimensions.borderBox()
    const { left, top, right, bottom } = layoutBox.dimensions.border
    const borderColor = getStyleValue(layoutBox, 'border-color')
    if (!borderColor) return

    ctx.fillStyle = borderColor

    // left
    ctx.fillRect(x, y, left, height)
    // top
    ctx.fillRect(x, y, width, top)
    // right
    ctx.fillRect(x + width - right, y, right, height)
    // bottom
    ctx.fillRect(x, y + height - bottom, width, bottom)
}
</code></pre><p>绘制边框，其实我们绘制的是四个矩形，每一个矩形就是一条边框。</p><h3 id="--23">绘制文本</h3><pre><code class="language-ts">function renderText(layoutBox: LayoutBox, ctx: CanvasRenderingContext2D, parent?: LayoutBox) {
    if (layoutBox.styleNode?.node.nodeType === NodeType.Text) {
        // get AnonymousBlock x y
        const { x = 0, y = 0, width } = parent?.dimensions.content || {}
        const styles = layoutBox.styleNode?.values || {}
        const fontSize = styles['font-size'] || '14px'
        const fontFamily = styles['font-family'] || 'serif'
        const fontWeight = styles['font-weight'] || 'normal'
        const fontStyle = styles['font-style'] || 'normal'

        ctx.fillStyle = styles.color
        ctx.font = `${fontStyle} ${fontWeight} ${fontSize} ${fontFamily}`
        ctx.fillText(layoutBox.styleNode?.node.nodeValue, x, y + parseInt(fontSize), width)
    }
}
</code></pre><p>通过 canvas 的 <code>fillText()</code> 方法，我们可以很方便的绘制带有字体风格、大小、颜色的文本。</p><h3 id="--24">输出图片</h3><p>绘制完成后，我们可以借助 <code>canvas</code> 的 API 输出图片。下面用一个简单的示例来演示一下：</p><pre><code class="language-html">&lt;html&gt;
    &lt;body id=" body " data-index="1" style="color: red; background: yellow;"&gt;
        &lt;div&gt;
            &lt;div class="lightblue test"&gt;test1!&lt;/div&gt;
            &lt;div class="lightblue test"&gt;
                &lt;div class="foo"&gt;foo&lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    &lt;/body&gt;
&lt;/html&gt;
* {
    display: block;
}

div {
    font-size: 14px;
    width: 400px;
    background: #fff;
    margin-bottom: 20px;
    display: block;
    background: lightblue;
}

.lightblue {
    font-size: 16px;
    display: block;
    width: 200px;
    height: 200px;
    background: blue;
    border-color: green;
    border: 10px;
}

.foo {
    width: 100px;
    height: 100px;
    background: red;
    color: yellow;
    margin-left: 50px;
}

body {
    display: block;
    font-size: 88px;
    color: #000;
}</code></pre><p>上面这段 HTML、CSS 代码经过渲染引擎程序解析后生成的图片如下：</p><figure class="kg-card kg-image-card"><img src="https://pic2.zhimg.com/80/v2-57e893eb457caeba4ceb5b70a77b8889_1440w.webp" class="kg-image" alt="v2-57e893eb457caeba4ceb5b70a77b8889_1440w" width="1000" height="500" loading="lazy"></figure><h3 id="--25">总结</h3><p>至此，这个玩具版的渲染引擎就完成了。虽然这个玩具并没有什么用，但如果能通过实现它来了解真实的渲染引擎是如何运作的，从这个角度来看，它还是“有用”的。</p><h3 id="--26">参考资料</h3><ul><li><a href="https://limpet.net/mbrubeck/2014/08/08/toy-layout-engine-1.html">Let's build a browser engine!</a></li><li><a href="https://github.com/mbrubeck/robinson">robinson</a></li><li><a href="https://developer.mozilla.org/zh-CN/docs/Web/Performance/How_browsers_work">渲染页面：浏览器的工作原理</a></li><li><a href="https://link.zhihu.com/?target=https%3A//developer.mozilla.org/zh-CN/docs/Web/Performance/Critical_rendering_path" rel="nofollow noreferrer">关键渲染路径</a></li><li><a href="https://link.zhihu.com/?target=https%3A//book.douban.com/subject/1998341/" rel="nofollow noreferrer">计算机系统要素</a></li></ul> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 在浏览器点击 URL 之后发生了什么？ ]]>
                </title>
                <description>
                    <![CDATA[ 在这篇文章中，我希望给读者建立一个网络世界的基本印象。我之前的文章都是互联网相关更高阶的内容，如：Angular 之旅 [https://www.freecodecamp.org/news/angular-a-journey-into-one-of-the-most-popular-front-end-tools-in-todays-job-market/] 、React 基础 [https://www.freecodecamp.org/news/start-your-journey-into-the-world-of-react-by-learning-these-basics-d6e05d3655e3/] 等。但在今天这篇文章中，我想带领各位读者一起领略点击 URL 之后的风光。 正如文章标题所示 - 当你点击 URL 之后会发生什么？——让我们开始吧！ 在探讨点击 URL 之后，我们得先了解 URL 是什么，它的各个组成部分，对不对？那么别浪费时间了，让我们开始了解 URL 吧！ URL – 统一资源定位符（Uniform Resource Locator） 正如 URL  ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/what-happens-when-you-hit-url-in-your-browser/</link>
                <guid isPermaLink="false">6384c576832e3f07817636d0</guid>
                
                    <category>
                        <![CDATA[ DNS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 浏览器 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ PapayaHUANG ]]>
                </dc:creator>
                <pubDate>Mon, 28 Nov 2022 04:18:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2022/11/browser.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/what-happens-when-you-hit-url-in-your-browser/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">What happens when you click on a URL in your browser</a>
      </p><!--kg-card-begin: markdown--><p>在这篇文章中，我希望给读者建立一个网络世界的基本印象。我之前的文章都是互联网相关更高阶的内容，如：<a href="https://www.freecodecamp.org/news/angular-a-journey-into-one-of-the-most-popular-front-end-tools-in-todays-job-market/">Angular 之旅</a>、<a href="https://www.freecodecamp.org/news/start-your-journey-into-the-world-of-react-by-learning-these-basics-d6e05d3655e3/">React 基础</a>等。但在今天这篇文章中，我想带领各位读者一起领略点击 URL 之后的风光。</p>
<p>正如文章标题所示 - <strong>当你点击 URL 之后会发生什么？</strong>——让我们开始吧！</p>
<p>在探讨<strong>点击 URL 之后</strong>，我们得先了解 URL 是什么，它的各个组成部分，对不对？那么别浪费时间了，让我们开始了解 URL 吧！</p>
<h2 id="urluniformresourcelocator">URL – 统一资源定位符（Uniform Resource Locator）</h2>
<p>正如 URL 的全称所述：它表示了我们想要访问的资源的地址。它相当于一个物理世界的<strong>地址</strong>，而在这个地址有你想要交互或者搜寻的信息。</p>
<p>在你的日常生活中，如果你出于工作或者讯息需求想要拜访一位朋友，你首先需要他/她的住址。在网络世界也是如此，你想要访问的网站必须有一个地址。<strong>网站就相当于朋友的房子，而 URL 就是房子的地址</strong>。</p>
<h3 id="url">剖析 URL</h3>
<p>现在我们已经清楚了 URL 是什么，但还不了解它的组成部分，让我开始吧！</p>
<p>看看这个例子：</p>
<p><a href="https://www.example.com/page1">https://www.example.com/page1</a></p>
<p>在示例中，第一个部分是 <strong>‘https’</strong>，它告诉浏览器应该使用哪种协议。协议可以是 <strong>http、https、ftp</strong> 等。<strong>协议</strong>是浏览器用于网络通信的<strong>一组规则</strong>。<strong>'https'</strong> 是安全版本，信息以一种安全的方式交换。</p>
<p>第二部分 <strong>www.example.com</strong> 是<strong>域名</strong>。你可以把它当作你朋友的房子。域名是网站的地址。我们使用域名来访问服务器（训练后的电脑），服务器的任务是给网站提供信息。稍等！我刚刚才说过 URL 是地址，现在又说域名也是地址。可别搞混了！</p>
<h3 id="url">URL 和域名之间的区别</h3>
<p>两者之间最大的不同是 <strong>URL 是完整的地址</strong>。 URL 提供了信息交流的方法以及登录网站之后的路径。而<strong>域名是 URL 的一部分</strong>。</p>
<p>让我们通过之前的示例来了解。你可以认为你朋友的住址是域名，同时 URL 不仅告知了朋友房子的住址（域名），以及你们交流的方式，如去到一个单独的房间（安全）或者当着所有人的面（信息可能会泄漏）。它也告知了路径，即到达房子之后，你应该去哪个房间。因此，域名是 URL 的一部分。一个包含域名和更多信息的地址才是 URL。</p>
<p>希望现在你更清楚 URL 是什么了，让我们进入下一个部分。</p>
<h2 id="">域名</h2>
<p>上文我介绍了域名，但未做深入讨论。现在就来看看，域名是网站的地址。它是浩瀚网络世界中<strong>网站</strong>的<strong>唯一标识</strong>。不能有两个相同的域名，但是 - 对！还有一个“但是”。 域名之间不仅仅是通过这个来区别的，还有其他的内容。</p>
<p>当点击 URL，也就是域名之后，网站被打开，内容呈现出来。每一台计算机都有 IP 地址，用来在互联网通信。正如它的名字“IP 地址”所述，它就是一个地址。当我们<strong>点击</strong>任意<strong>URL</strong>时，我们实际上是在 <strong>点击</strong> 负责提供网站内容（托管）的电脑的 <strong>IP 地址</strong>。</p>
<p>现在你可能会觉得，什么鬼！所有东西都是地址？有 IP 地址的话，还要域名干嘛？为什么不直接用 IP 地址获取内容？</p>
<p>是的！你可以<strong>使用 IP 地址</strong>来<strong>获取内容</strong>，但你真的记得住所有网站对应的 IP 地址吗？显然不行！记住每一个网站<strong>的 IP 地址</strong>可<strong>太难了</strong>，域名才应运而生。</p>
<p>你可以类比通讯录。你记不住每一个人的手机号码，但是你可以记住他们的名字。域名和 IP 地址的关系也同样。你<strong>可记不住</strong>这些吓死人的 <strong>IP 地址</strong>，但是你可以轻松<strong>记住域名</strong>。</p>
<p>大量的数据由数据库维护，并且存储在域名下这个 IP 地址内。存储域名和对应 IP 地址的系统被称为 <strong>DNS（域名系统）</strong> 。（我相信你肯定听说过）</p>
<p>基础介绍得差不多了，让我们进入点击 URL 之后的步骤。</p>
<h2 id="dnslookupip">DNS lookup 查找 IP 地址</h2>
<p>访问 URL 之后，首先要做的是解析与域名相关联的 IP 地址。DNS 辅助这个工作。<strong>DNS 就像电话簿一样，给我们提供与域名关联的 IP 地址</strong>，就如同我电话簿提供和人名相关的手机号码一样。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/06/dns.png" alt="dns" width="600" height="400" loading="lazy"></p>
<p>这是概述，但此域名查询通过<strong>四层</strong>，分以下几个步骤 ：</p>
<p>1. 点击 URL 之后，将检查<strong>浏览器缓存</strong>。因为浏览器会在一段时间内存储你访问过的网站的 DNS 记录，DNS 查询会先查缓存里的与域名关联的 IP 地址。</p>
<p>2. DNS 查询检查的第二个位置是<strong>系统缓存</strong>，其次是<strong>路由缓存</strong>。</p>
<p>3. 如果在上述步骤中，DNS 查询未得到解析，则需要解析服务器的帮助。解析器服务器只不过是 ISP（Internet 服务提供商）。查询被发送到 ISP，DNS 查询在 <strong>ISP 缓存</strong>中运行。</p>
<p>4. 如果在第 3 步中也没有找到结果，则将请求发送到 DNS 层级结构的<strong>顶层或者根服务器</strong>。从来没有发生过在这个阶段没有找到结果的情况，实际上，这部分告诉你可以从那儿找到信息。如果你正在搜索顶级域（.com、.net、.Gov、.org）的 IP 地址，就是告诉解析器服务器搜索 <strong>TLD 服务器</strong>（顶级域）。</p>
<p>5. 现在，解析器要求 TLD 服务器提供域名的 IP 地址。 TLD 存储域名的地址信息。它告诉解析器向<strong>权威性域名服务器</strong>询问。</p>
<p>6. 权威性域名服务器负责了解域名的所有信息。最终，解析器（ISP）获取与域名相关联的 IP 地址，并将其发送回浏览器。</p>
<p>获取 IP 地址后，解析器将其存储在缓存中，这样下次如果出现相同的查询，就不必再次执行所有这些步骤。它可以从缓存得到 IP 地址。</p>
<p>这就是解析与域名关联的 IP 地址所遵循的所有步骤。可以结合下图理解：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/06/dns_resolve.png" alt="dns_resolve" width="600" height="400" loading="lazy"></p>
<h2 id="tcp">由浏览器发起的与服务器的 TCP 连接</h2>
<p>一旦计算机的<strong>IP 地址</strong>（网站信息存储的地方）被<strong>发现</strong>，就会<strong>启动连接</strong>。网络通信遵循<strong>互联网协议</strong>。<strong>TCP/IP</strong> 就是最常见的协议。浏览器和服务器之间的通讯遵循 <strong>‘TCP 三次握手’</strong>。让我们简短地了解一下这个过程：</p>
<p>1. 客户端计算机发送一个<strong>SYN 消息</strong>，确认另一台计算机是否可以建立连接。</p>
<p>2. 之后来到<strong>另一台计算机</strong>，如果可以建立新的连接，会同时发送 SYN 消息和 <strong>确认消息</strong>。</p>
<p>3. 在此之后，<strong>第一台计算机</strong>接收到它的消息，并通过<strong>发送 ACK 消息</strong>进行确认。</p>
<p>可以通过下图理解：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/06/process.png" alt="process" width="600" height="400" loading="lazy"></p>
<h2 id="">通信开始（请求响应过程）</h2>
<p>最终客户端和服务器之间的连接建立，双方可以相互交流信息。连接成功之后，<strong>浏览器(客户端)<strong>向</strong>服务器</strong>发送<strong>请求</strong>，告诉服务器需要什么内容。服务器知道应该对每一种请求做何种反应。因此<strong>服务器响应客户端</strong>，响应包含了请求的所有内容，包括网页、状态码、缓存控制等。然后浏览器渲染请求内容。</p>
<p>就这么多，当你点击 URL 之后，上面的所有步骤就运转起来，当然这些步骤可能在一秒之内就完成了。这就回答了“当我们点击 URL 之后，发生了什么？”这个问题。</p>
<p>感谢你阅读本文！</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 浏览器技术架构的演进过程和背景 ]]>
                </title>
                <description>
                    <![CDATA[ 背景 当下的前端工程师可能接触最多的就是chrome浏览器了。但是可能大部分同学不清楚其内部运行的机制，以及这些年浏览器到底进行了哪些优化，才得以支撑互联网世界庞大的业务量，以及给广大用户网上冲浪的极致体验。本文不会去写深入底层的实现，而是想通过描述浏览器的技术演进来知道它的背景，以及明白在开发和使用浏览器应用时遇到的问题的背后原因。  * 为什么现代浏览器相对来说比较稳定，而早期的浏览器极易容易崩溃？  * 为什么现代web应用比较流畅可以做混合应用堪比原生，而早期的web页面会高频卡顿？  * 为什么早在2009年前web网站上各种插件横行，且经常爆出安全问题？ 在2007年前后就已经上网的同学应该玩过QQ空间、百度贴吧或者校内网(人人网)等，那个时候的PC浏览器打开页面需要等待好久，手机打开网页速度慢的体感更强。当然在一定程度上也取决于当时的移动网络为2g网络，而PC网络多为有限宽带。网络因素、硬件设备配置等因素我们今天先不做深究，今天我们主要的主题是探讨浏览器本身的技术架构演进过程。 单进程浏览器 在了解单进程浏览器之前，大家需要先了解下什么是进程，什么是线程？这里就不在 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/the-evolution-process-and-background-of-browser-technology-architecture/</link>
                <guid isPermaLink="false">61a5f033f942e1068cb34646</guid>
                
                    <category>
                        <![CDATA[ 浏览器 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Topqiang ]]>
                </dc:creator>
                <pubDate>Mon, 29 Nov 2021 09:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/11/glenn-carstens-peters-npxXWgQ33ZQ-unsplash.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <h2 id="-">背景</h2><p>当下的前端工程师可能接触最多的就是chrome浏览器了。但是可能大部分同学不清楚其内部运行的机制，以及这些年浏览器到底进行了哪些优化，才得以支撑互联网世界庞大的业务量，以及给广大用户网上冲浪的极致体验。本文不会去写深入底层的实现，而是想通过描述浏览器的技术演进来知道它的背景，以及明白在开发和使用浏览器应用时遇到的问题的背后原因。</p><ul><li>为什么现代浏览器相对来说比较稳定，而早期的浏览器极易容易崩溃？</li><li>为什么现代web应用比较流畅可以做混合应用堪比原生，而早期的web页面会高频卡顿？</li><li>为什么早在2009年前web网站上各种插件横行，且经常爆出安全问题？</li></ul><p>在2007年前后就已经上网的同学应该玩过QQ空间、百度贴吧或者校内网(人人网)等，那个时候的PC浏览器打开页面需要等待好久，手机打开网页速度慢的体感更强。当然在一定程度上也取决于当时的移动网络为2g网络，而PC网络多为有限宽带。网络因素、硬件设备配置等因素我们今天先不做深究，今天我们主要的主题是探讨浏览器本身的技术架构演进过程。</p><h2 id="--1">单进程浏览器</h2><p>在了解单进程浏览器之前，大家需要先了解下什么是进程，什么是线程？这里就不在赘述了大家可以<a href="https://link.juejin.cn?target=https%3A%2F%2Fyuque.antfin-inc.com%2Fziqiang.kzq%2Fhh86sm%2Fmwcqv1" rel="nofollow noopener noreferrer">自行了解下</a>进程和线程之后，我们再来一起看下单进程浏览器的架构。</p><p>单进程浏览器是指浏览器的所有功能都在同一个进程里运行，这其中包括了网络、插件、JavaScript 运行环境、渲染引擎和页面等。其实在2007年前后，市面上浏览器大部分都是单进程的。单进程浏览器的架构如下图所示：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/11/11.jpg" class="kg-image" alt="11" width="499" height="204" loading="lazy"></figure><p>如此多的功能运行在一个进程里，是导致单进程浏览器不稳定、不流畅和不安全的一个主要因素。下面我就来一一分析下出现这些问题的原因。</p><h3 id="--2">不稳定</h3><p>早期浏览器的视频、游戏需要借助于插件来实现诸如 Flash 播放器等各种强大的功能，但是插件是最容易出问题的模块，并且还运行在浏览器进程之中，所以一个插件的意外崩溃会引起整个浏览器的崩溃。除了插件之外，渲染引擎模块也是不稳定的，通常一些复杂的 JavaScript 代码就有可能引起渲染引擎模块的崩溃。和插件一样，渲染引擎的崩溃也会导致整个浏览器的崩溃。</p><h3 id="--3">不流畅</h3><p>从上面的“单进程浏览器架构示意图”可以看出，所有页面的渲染模块、JavaScript 执行环境以及插件都是运行在同一个线程中的，这就意味着同一时刻只能有一个模块可以执行。比如，下面这个无限循环的脚本：</p><pre><code>   function freeze() { 
     while (1) { 
       console.log("freeze"); 
     }
   }
   freeze();</code></pre><p>如果让这个脚本运行在一个单进程浏览器的页面里，你感觉会发生什么？因为这个脚本是无限循环的，所以当其执行时，它会独占整个线程，这样导致其他运行在该线程中的模块就没有机会被执行。因为浏览器中所有的页面都运行在该线程中，所以这些页面都没有机会去执行任务，这样就会导致整个浏览器失去响应，变卡顿。这块内容要继续往深的地方讲就到页面的<a href="https://link.juejin.cn?target=https%3A%2F%2Fzhuanlan.zhihu.com%2Fp%2F145383822" rel="nofollow noopener noreferrer">事件循环</a>系统了。除了上述脚本或者插件会让单进程浏览器变卡顿外，页面的内存泄漏也是单进程变慢的一个重要原因。通常浏览器的内核都是非常复杂的，运行一个复杂点的页面再关闭页面，会存在内存不能完全回收的情况，这样导致的问题是使用时间越长，内存占用越高，浏览器会变得越慢。</p><h3 id="--4">不安全</h3><p>这里依然可以从插件和页面脚本两个方面来解释该原因。插件可以使用 C/C++ 等代码编写，通过插件可以获取到操作系统的任意资源，当你在页面运行一个插件时也就意味着这个插件能完全操作你的电脑。如果是个恶意插件，那么它就可以释放病毒、窃取你的账号密码，引发安全性问题。至于页面脚本，它可以通过浏览器的漏洞来获取系统权限，这些脚本获取系统权限之后也可以对你的电脑做一些恶意的事情，同样也会引发安全问题。以上这些就是当时浏览器的特点，不稳定，不流畅，而且不安全。</p><h2 id="--5">多进程浏览器</h2><p>你可以先看看下面这张图，这是 2008 年 Chrome 发布时的进程架构。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/11/22.jpg" class="kg-image" alt="22" width="500" height="317" loading="lazy"></figure><p>从图中可以看出，Chrome 的页面是运行在单独的渲染进程中的，同时页面里的插件也是运行在单独的插件进程之中，而进程之间是通过 IPC 机制进行通信（如图中虚线部分）。</p><p>我们先看看如何解决不稳定的问题。由于进程是相互隔离的，所以当一个页面或者插件崩溃时，影响到的仅仅是当前的页面进程或者插件进程，并不会影响到浏览器和其他页面，这就完美地解决了页面或者插件的崩溃会导致整个浏览器崩溃，也就是不稳定的问题。</p><p>我提到 Chrome 使用多个渲染器进程。在最简单的情况下，您可以想象每个选项卡都有自己的渲染器进程。假设您打开了 3 个选项卡，每个选项卡都由一个独立的渲染器进程运行。如果一个选项卡变得无响应，那么您可以关闭无响应的选项卡并继续使用其他选项卡的服务,如果所有选项卡都在一个进程上运行，当一个选项卡变得无响应时，所有选项卡都无响应。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/11/33.jpg" class="kg-image" alt="33" width="500" height="282" loading="lazy"></figure><p>对于内存泄漏的解决方法那就更简单了，因为当关闭一个页面时，整个渲染进程也会被关闭，之后该进程所占用的内存都会被系统回收，这样就轻松解决了浏览器页面的内存泄漏问题。最后我们再来看看上面的两个安全问题是怎么解决的。采用多进程架构的额外好处是可以使用安全沙箱，你可以把沙箱看成是操作系统给进程上了一把锁，沙箱里面的程序可以运行，但是不能在你的硬盘上写入任何数据，也不能在敏感位置读取任何数据，例如你的文档和桌面。Chrome 把插件进程和渲染进程锁在沙箱里面，这样即使在渲染进程或者插件进程里面执行了恶意程序，恶意程序也无法突破沙箱去获取系统权限。</p><h3 id="--6">有哪些进程存在</h3><p>不过 Chrome 的发展是滚滚向前的，相较之前，目前的架构又有了很多新的变化。我们先看看最新的 Chrome 进程架构，你可以参考下图：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/11/44.jpg" class="kg-image" alt="44" width="500" height="354" loading="lazy"></figure><p>图中是我从 Chrome/94 浏览器中截图出来的，大家可以看到最新的 Chrome 浏览器包括：1 个 GPU 进程、1 个 V8代理解析工具（v8代码解释器）、1 个NetWork进程、1 个浏览器主进程、多个插件进程(我插件装的太多)、多个渲染进程和备用渲染进程。<br>下面我们来逐个分析下这几个进程的功能。</p><ul><li>GPU 进程。Chrome 刚开始发布的时候是没有 GPU 进程的。而 GPU 的使用初衷是为了实现 3D CSS 的效果，只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制，这使得 GPU 成为浏览器普遍的需求。最后，Chrome 在其多进程架构上也引入了GPU 进程。</li><li>V8代理解析工具（v8代码解释器）Google的开源高性能JavaScript和WebAssembly引擎，用C编写，它实现ECMAScript和WebAssembly，可独立运行或嵌入到任何C应用程序中，如Chrome和Node.js。</li><li>NetWork进程。主要负责页面的网络资源加载，之前是作为一个模块运行在浏览器进程里面的，直至最近才独立出来，成为一个单独的进程。</li><li>浏览器进程。主要负责界面显示、用户交互、子进程管理，同时提供存储等功能。</li><li>扩展程序(插件进程)。主要是负责插件的运行，因插件易崩溃，所以需要通过插件进程来隔离，以保证插件进程崩溃不会对浏览器和页面造成影响。</li><li>渲染进程。核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页，排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中，默认情况下，Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑，渲染进程都是运行在沙箱模式下。</li><li>GUI渲染线程：负责渲染页面，布局和绘制、页面需要重绘和回流时，该线程就会执行、与js引擎线程互斥，防止渲染结果不可预期</li><li>JS引擎线程：负责处理解析和执行javascript脚本程序、只有一个JS引擎线程（单线程）、与GUI渲染线程互斥，防止渲染结果不可预期</li><li>事件触发线程：用来控制事件循环（鼠标点击、setTimeout、ajax等）当处理一些不能立即执行的代码时，会将对应的任务在其可以触发的时机，添加到事件队列的末端事件循环机制会在JS引擎线程空闲时，循环访问事件队列的头部，如果有函数，则会将该函数推到执行栈中并立即执行</li><li>定时触发器线程：setInterval与setTimeout所在的线程、定时任务并不是由JS引擎计时的，是由定时触发线程来计时的、计时完毕后，将回调事件放入到事件队列中</li><li>异步http请求线程：浏览器有一个单独的线程用于处理AJAX请求、当请求完成时，若有回调函数，将回调事件放入到事件队列中。</li></ul><h3 id="--7">渲染进程的分配</h3><p>其时浏览器的渲染进程划分，还会多一个维度，那就是按照站点维度进行划分：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/11/55.jpg" class="kg-image" alt="55" width="500" height="262" loading="lazy"></figure><p>站点隔离是 Chrome 中最近引入的一项功能，它为每个跨站点 iframe 运行单独的渲染器进程。我们一直在讨论每个选项卡模型一个渲染器进程，它允许跨站点 iframe 在单个渲染器进程中运行，并在不同站点之间共享内存空间。在同一个渲染器进程中运行 a.com 和 b.com 似乎没问题。同源策略是网络的核心安全模型；它确保一个站点未经同意无法访问其他站点的数据。绕过此策略是安全攻击的主要目标。进程隔离是最有效的站点分离方式。使用 <a href="https://link.juejin.cn?target=https%3A%2F%2Fdevelopers.google.com%2Fweb%2Fupdates%2F2018%2F02%2Fmeltdown-spectre" rel="nofollow noopener noreferrer">Meltdown 和 Spectre</a>，我们需要使用流程来分离站点变得更加明显。自 Chrome 67 以来，桌面上默认启用站点隔离，选项卡中的每个跨站点 iframe 都有一个单独的渲染器进程。</p><p>启用站点隔离是一项多年的工程努力。站点隔离并不像分配不同的渲染器进程那么简单；它从根本上改变了 iframe 相互通信的方式。在不同进程上运行 iframe 的页面上打开 devtools 意味着 devtools 必须实施幕后工作以使其看起来无缝。即使运行简单的 Ctrl+F 来查找页面中的单词也意味着在不同的渲染器进程中进行搜索。您可以看到浏览器工程师将 Site Isolation 的发布称为重大里程碑的原因。</p><p>但也不完全是相同站点就一定会在同一个渲染进程中，不同的tab就一定不在同一个渲染进程中。这个具体的进程分配逻辑取决于Chromium的处理模型(process-models)决定，而Chrome的默认处理模式就是Process-per-site-instance。 <strong>Process-per-site-instance :</strong> &nbsp;当你打开一个 tab 访问 <a href="https://link.juejin.cn?target=https%3A%2F%2Fa.baidu.com" rel="nofollow noopener noreferrer">a.baidu.com</a>，然后再打开一个 tab 访问 <a href="https://link.juejin.cn?target=https%3A%2F%2Fb.baidu.com" rel="nofollow noopener noreferrer">b.baidu.com</a>，这两个 tab 会使用两个进程。如果 <a href="https://link.juejin.cn?target=https%3A%2F%2Fb.baidu.com" rel="nofollow noopener noreferrer">b.baidu.com</a> 是通过 <a href="https://link.juejin.cn?target=https%3A%2F%2Fa.baidu.com" rel="nofollow noopener noreferrer">a.baidu.com</a> 页面的 JavaScript 代码打开的，这两个 tab 会使用同一个进程，比如下图的例子，可以看到两个 tab 的 processId 是相同的。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/11/66.jpg" class="kg-image" alt="66" width="500" height="263" loading="lazy"></figure><p>除此之外还有三种处理模型【Process-per-site、Process-per-tab、Single process】，大家感兴趣可以查看<a href="https://link.juejin.cn?target=https%3A%2F%2Fwww.chromium.org%2Fdevelopers%2Fdesign-documents%2Fprocess-models" rel="nofollow noopener noreferrer">chromium官方相关介绍</a>。</p><h3 id="--8">现阶段存在的缺陷</h3><p>凡事都有两面性，虽然面向服务的多进程架构模型提升了浏览器的稳定性、流畅性和安全性，但同样不可避免地带来了一些问题：</p><ul><li>更高的资源占用。因为每个进程都会包含公共基础结构的副本（如 JavaScript 运行环境），这就意味着浏览器会消耗更多的内存资源。</li><li>更复杂的体系架构。浏览器各模块之间耦合性高、扩展性差等问题，会导致现在的架构已经很难适应新的需求了。</li></ul><p>为了更好的均衡各方面诉求，我们来看下 Chrome 为了节省更多内存，所做的服务化。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/11/77.jpg" class="kg-image" alt="77" width="498" height="154" loading="lazy"></figure><p>Chrome 正在经历架构更改以将浏览器程序的每个部分作为服务运行，从而可以轻松拆分为不同的进程或聚合为一个。一般的想法是，当 Chrome 在强大的硬件上运行时，它可能会将每个服务拆分为不同的进程，从而提供更高的稳定性，但如果是在资源受限的设备上，Chrome 会将服务整合到一个进程中，从而节省内存占用。在此更改之前，已在 Android 等平台上使用了类似的方法来合并进程以减少内存使用量。</p><p>当然上面所讲的这些并不是问题的全部原因，这些年发展的也并不只是浏览器架构，还有网络协议，基站，芯片，cpu，带宽承载能力等科技领域的发展，才得以让PC/移动互联网体验更好的应用服务我们的生活，我们才得以感受到数字化生活的便利。</p><h2 id="--9">参考文档</h2><ul><li><a href="https://developers.google.com/web/updates/2018/09/inside-browser-part1">https://developers.google.com/web/updates/2018/09/inside-browser-part1</a></li><li><a href="https://www.chromium.org/developers/design-documents/process-models">https://www.chromium.org/developers/design-documents/process-models</a></li><li>李兵的<a href="https://time.geekbang.org/column/intro/100033601">《浏览器工作原理与实践》</a></li></ul> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 如何编写浏览器扩展（含示例项目） ]]>
                </title>
                <description>
                    <![CDATA[ 在本文中，我们将讨论浏览器扩展——它们是什么、它们如何工作，以及如何构建。 我们将通过实际编写自己的扩展（超级有趣！）来完成，它允许我们通过单击一个按钮将任何代码片段复制到剪贴板。 开始之前：  * 你需要对 JavaScript 有基本的了解  * 你需要 Firefox 浏览器（或任何其他浏览器也可以） 什么是浏览器扩展 浏览器扩展是你添加到浏览器中的内容，它通过扩展浏览器的容量来增强你的浏览体验。 例如，考虑一下你可能已在设备上安装的广告拦截器，这可以通过在你上网时屏蔽广告来改善你的浏览体验。 如何编写浏览器扩展 现在让我们从编写一个非常基本的扩展开始。 首先，我们将创建一个文件夹，在其中创建一个名为 manifest.json 的文件。 什么是 manifest 文件？ manifest 文件是任何浏览器扩展中的必备文件。此文件包含有关扩展程序的基本数据，如名称、版本等。 现在在 manifest.json 文件中复制以下代码段： {   "manifest_version":2,   "version":"1.0",   "name":"Test", }  ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/write-your-own-browser-extensions/</link>
                <guid isPermaLink="false">618253eb4aadd7063f2ba246</guid>
                
                    <category>
                        <![CDATA[ 浏览器 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 效率 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Chengjun.L ]]>
                </dc:creator>
                <pubDate>Thu, 11 Nov 2021 07:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/11/browsers.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>在本文中，我们将讨论浏览器扩展——它们是什么、它们如何工作，以及如何构建。</p><p>我们将通过实际编写自己的扩展（超级有趣！）来完成，它允许我们通过单击一个按钮将任何代码片段复制到剪贴板。</p><p>开始之前：</p><ul><li>你需要对 JavaScript 有基本的了解</li><li>你需要 Firefox 浏览器（或任何其他浏览器也可以）</li></ul><h2 id="-">什么是浏览器扩展</h2><p>浏览器扩展是你添加到浏览器中的内容，它通过扩展浏览器的容量来增强你的浏览体验。</p><p>例如，考虑一下你可能已在设备上安装的广告拦截器，这可以通过在你上网时屏蔽广告来改善你的浏览体验。</p><h2 id="--1">如何编写浏览器扩展</h2><p>现在让我们从编写一个非常基本的扩展开始。</p><p>首先，我们将创建一个文件夹，在其中创建一个名为 <code>manifest.json</code> 的文件。</p><h3 id="-manifest-">什么是 manifest 文件？</h3><p>manifest 文件是任何浏览器扩展中的必备文件。此文件包含有关扩展程序的基本数据，如名称、版本等。</p><p>现在在 <code>manifest.json</code> 文件中复制以下代码段：</p><pre><code class="language-json">{
  "manifest_version":2,
  "version":"1.0",
  "name":"Test",
}
</code></pre><h3 id="--2"><strong>如何加载扩展文件</strong></h3><p>对于 Firefox 用户，请按照以下步骤操作。</p><p>在地址栏中，搜索：</p><pre><code>about:debugging#/runtime/this-firefox</code></pre><p>你将看到一个 <em>Load Temporary Add-on（加载临时加载项）</em>的选项。单击该选项并从目录中选择 <code>manifest.json</code> 文件。</p><p>对于 Chrome 用户，在地址栏中搜索：</p><pre><code>chrome://extensions.</code></pre><ul><li>启用开发者模式并切换到它</li><li>单击 Load unpacked 按钮并选择扩展目录</li></ul><p>太棒了，你已成功安装扩展程序。但该扩展目前没有做任何事情。现在让我们为扩展添加一些功能。为此，我们将像这样编辑 <code>manifest.json</code> 文件：</p><pre><code class="language-json">{
  "manifest_version":2,
  "version":"1.0",
  "name":"Test",
  "content_scripts":[
    {
     "matches":["&lt;all_urls&gt;"],
     "js":["main.js"]
    }
  ]
}</code></pre><p>在上面的代码中，我们向 <code>manifest.json</code> 添加了一个内容脚本。内容脚本可以操作网页的文档对象模型。我们可以使用内容脚本将 JS（和 CSS）注入到网页中。</p><p><code>"matches"</code> 包含应添加内容脚本的域和子域列表，<code>js</code> 是要加载的 JS 文件的数组。</p><p>现在在同一目录中创建一个 <code>main.js</code> 文件，并添加以下代码：</p><pre><code class="language-js">alert("The test extension is up and running")</code></pre><p>现在重新加载扩展程序，当你访问任何 URL 时，你将看到一条警报消息。</p><p><strong>不要忘记在编辑代码时重新加载扩展。</strong></p><h2 id="--3">如何自定义你的浏览器扩展</h2><p>现在让我们为扩展程序添加更多有趣的东西。</p><p>我们现在要做的是创建一个网络扩展程序，将我们访问的网页的所有图像更改为我们选择的图像。</p><p>为此，只需将任何图像添加到当前目录，并将 <code>main.js</code> 文件更改为：</p><pre><code class="language-js">console.log("The extension is up and running");

var images = document.getElementsByTagName('img')

for (elt of images){
   elt.src = `${browser.runtime.getURL("pp.jpg")}`
   elt.alt = 'an alt text'
}</code></pre><p>让我们看看这里发生了什么：</p><pre><code class="language-js">var images = document.getElementsByTagName('img')</code></pre><p>这行代码选择了网页中所有带有 <code>img</code> 标签的元素。</p><p>然后我们使用 for 循环遍历数组图像，在 <code>runtime.getURL</code> 函数的帮助下将所有 <code>img</code> 元素的 <code>src</code> 属性更改为一个 URL。</p><p>这里 <code>pp.jpg</code> 是我的设备当前目录下的图片文件名。</p><p>我们通过将 <code>manifest.json</code> 文件编辑为以下内容来通知我们的内容脚本关于 <code>pp.jpg</code> 文件：</p><pre><code class="language-js">{
  "manifest_version":2,
  "version":"1.0",
  "name":"Test",
  "content_scripts":[
   {
    "matches":["&lt;all_urls&gt;"],
    "js":["main.js"]
   }
  ],
  "web_accessible_resources": [
        "pp.jpg"
  ]
}
</code></pre><p>然后只需重新加载扩展程序，并访问你喜欢的任何 URL。现在你应该看到所有图像都被更改为当前工作目录中的图像。</p><h3 id="--4">如何将图标添加到你的扩展程序</h3><p>在 <code>manifest.json</code> 文件中添加以下代码：</p><pre><code class="language-json">"icons": {
  "48": "icon-48.png",
  "96": "icon-96.png"
}
</code></pre><h3 id="--5">如何向扩展程序添加工具栏按钮</h3><p>现在我们将在浏览器的工具栏中添加一个按钮，用户可以使用此按钮与扩展程序交互。</p><p>要添加工具栏按钮，请将以下几行添加到 <code>manifest.json</code> 文件中：</p><pre><code class="language-json">"browser_action":{
   "default_icon":{
     "19":"icon-19.png",
     "38":"icon-38.png"
   }
  }</code></pre><p>所有图像文件都应该存在于你的当前目录中。</p><p>现在，如果我们重新加载扩展程序，应该会在浏览器的工具栏中看到扩展程序的图标。</p><h3 id="--6">如何为工具栏按钮添加监听事件</h3><p>也许我们想在用户点击按钮时做一些事情——假设我们想在每次点击按钮时打开一个新标签。</p><p>为此，我们将再次将以下内容添加到 <code>manifest.json</code> 文件中：</p><pre><code class="language-json">"background":{
        "scripts":["background.js"]
  },
  "permissions":[
      "tabs"
  ]
</code></pre><p>然后我们将在当前工作目录中创建一个名为 <code>background.js</code> 的新文件，并在文件中添加以下几行：</p><pre><code class="language-js">function openTab(){
    
    var newTab = browser.tabs.create({
        url:'https://twitter.com/abhilekh_gautam',
        active:true
    })
}

browser.browserAction.onClicked.addListener(openTab)
</code></pre><p>现在重新加载扩展！</p><p>每当有人单击该按钮时，它都会调用 <code>openTab</code> 函数，打开一个带有链接到我的 Twitter 个人资料的 URL 的新选项卡。此外，当设置为 true 时，活动键使新创建的选项卡成为当前选项卡。</p><p>请注意，你可以在后台脚本中使用浏览器提供的 API。有关 API 的更多信息，请参阅文章：<a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API">JavaScript API</a>。</p><p>现在我们已经了解了浏览器扩展的一些基础知识，让我们创建一个我们作为开发人员可以在日常生活中使用的扩展。</p><h2 id="--7">项目</h2><p>好的，现在我们要写一些日常生活中用得上的东西。我们将创建一个扩展程序，允许你通过单击从 StackOverflow 复制代码片段。 因此，我们的扩展程序将向网页添加一个 <code>Copy</code> 按钮，该按钮将代码复制到剪贴板。</p><h3 id="demo"><strong>Demo</strong></h3><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/2021/10/demo.png" class="kg-image" alt="demo" width="600" height="400" loading="lazy"></figure><p>首先，我们将创建一个新文件夹/目录，我们将在其中添加一个 <code>manifest.json</code> 文件。</p><p>将以下代码添加到文件中：</p><pre><code class="language-json">
  "manifest_version":2,
  "version":"1.0",
  "name":"copy code",
  "content_scripts":[
    {
     "matches":["*://*.stackoverflow.com/*"],
     "js":["main.js"]
    }
  ]
}</code></pre><p>查看 <code>content script</code> 中的 <code>matches</code>——扩展仅适用于 StackOverflow 的域名和子域名。</p><p>现在在同一目录中创建另一个名为 <code>main.js</code> 的 JavaScript 文件，并添加以下代码：</p><pre><code class="language-js">var arr =document.getElementsByClassName("s-code-block")

for(let i = 0 ; i &lt; arr.length ; i++){
 var btn = document.createElement("button")
 btn.classList.add("copy_code_button")
 btn.appendChild(document.createTextNode("Copy"))
 arr[i].appendChild(btn)
 //styling the button
 btn.style.position = "relative"
 
 if(arr[i].scrollWidth === arr[i].offsetWidth &amp;&amp; arr[i].scrollHeight === arr[i].offsetHeight)
  btn.style.left = `${arr[i].offsetWidth - 70}px`

  else if(arr[i].scrollWidth != arr[i].offsetWidth &amp;&amp; arr[i].scrollHeight === arr[i].offsetWidth)
   btn.style.left = `${arr[i].offsetWidth - 200}px`
 else 
   btn.style.left = `${arr[i].offsetWidth - 150}px`
  
 if(arr[i].scrollHeight === arr[i].offsetHeight)
   btn.style.bottom = `${arr[i].offsetHeight - 50}px`
   
 else
   btn.style.bottom = `${arr[i].scrollHeight - 50}px`
 //end of styling the button
   
   console.log("Appended")
}</code></pre><p>首先，我选择了所有类名为 <code>s-code-block</code> 的元素——但为什么呢？ 这是因为当我检查 StackOverflow 的站点时，发现所有代码片段都保存在一个具有该名称的类中。</p><p>然后我们遍历所有这些元素，并在这些元素中添加一个按钮。最后，我们正确定位和设置按钮样式（样式尚不完美——这只是一个开始）。</p><p>当我们使用上面的过程加载扩展并访问 StackOverflow 时，应该看到一个 copy 按钮。</p><h3 id="--8">如何为按钮添加功能</h3><p>现在，当单击按钮时，我们希望将整个片段复制到剪贴板。为此，请将以下代码行添加到 <code>main.js</code> 文件中：</p><pre><code class="language-js">var button = document.querySelectorAll(".copy_code_button")
 button.forEach((elm)=&gt;{
  elm.addEventListener('click',(e)=&gt;{
    navigator.clipboard.writeText(elm.parentNode.childNodes[0].innerText)
    alert("Copied to Clipboard")
  })
 })</code></pre><p>首先，我们使用 <code>querySelectorAll</code> 选择添加到站点的所有按钮。然后我们在点击按钮时监听 click 事件。</p><pre><code class="language-js">navigator.clipboard.writeText(elm.parentNode.childNodes[0].innerText)
</code></pre><p>上面的代码将代码复制到我们的剪贴板。每当复制片段时，我们都会通过消息 <code>Copied to clipboard</code> 来提醒用户，这样就完成了。</p><h2 id="--9"><strong>结语</strong></h2><p>Web 扩展可以通过多种方式发挥作用，我希望在本文的帮助下，你能够编写自己的扩展。</p><p>所有代码都可以在这个 <a href="https://github.com/Abhilekhgautam/Copy-Code">GitHub 仓库</a>中找到。如果你想建议一些好的样式或新功能（如剪贴板历史记录等），欢迎创建 pull request。</p><p><strong><strong>Happy Coding!</strong></strong></p><p>原文：<a href="https://www.freecodecamp.org/news/write-your-own-browser-extensions/">How to Write Your Own Browser Extension [Example Project Included]</a>，作者：<a href="https://www.freecodecamp.org/news/author/abhilekh/">Abhilekh Gautam</a></p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 浏览器是如何渲染网页的 ]]>
                </title>
                <description>
                    <![CDATA[ 今天我们讨论的话题将专注于网页渲染以及它在Web开发中至关重要的作用。其实网上已经有许多谈论这个主题的文章了，但大多数文章提供的都是比较碎片化的信息，我需要查阅相当多的资料，才能完整地了解网页渲染。所以我决定写下这篇有一定综合性的文章。相信一方面能够帮助初学者了解网页渲染的原理，另一方面也能帮助有经验的同学细化巩固相关的知识结构。 不同的浏览器引擎运行起来会有些许差异，针对特定浏览器的具体内容会更加复杂。本文并不会涉及某个浏览器的底层原理，而是讨论一些通共的原则。 浏览器如何渲染网页 我们先来了解一下浏览器是如何对网页进行渲染的：  1. 浏览器将从服务器获取的HTML文档构建成文档对象模型DOM(Document Object Model).  2. 样式将被载入和解析，构成层叠样式表模型CSSOM(CSS Object Model).  3. 在DOM和CSSOM之上，渲染树(rendering     tree)将会被创建，代表一系列将被渲染的对象（这在Webkit内核中被称为renderer或者渲染对象render     object，在Gecko内核中被称为框架fra ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/webpage-rendering/</link>
                <guid isPermaLink="false">5fa2b3d05f583f0565090d9e</guid>
                
                    <category>
                        <![CDATA[ 浏览器 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ DOM ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web开发 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ 余博伦 ]]>
                </dc:creator>
                <pubDate>Thu, 12 Nov 2020 09:20:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/04/photo-1605123308459-abb648aa4f66.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>今天我们讨论的话题将专注于网页渲染以及它在Web开发中至关重要的作用。其实网上已经有许多谈论这个主题的文章了，但大多数文章提供的都是比较碎片化的信息，我需要查阅相当多的资料，才能完整地了解网页渲染。所以我决定写下这篇有一定综合性的文章。相信一方面能够帮助初学者了解网页渲染的原理，另一方面也能帮助有经验的同学细化巩固相关的知识结构。</p><p>不同的浏览器引擎运行起来会有些许差异，针对特定浏览器的具体内容会更加复杂。本文并不会涉及某个浏览器的底层原理，而是讨论一些通共的原则。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/11/image-15.png" class="kg-image" alt="image-15" width="1150" height="537" loading="lazy"></figure><h2 id="-">浏览器如何渲染网页</h2><p>我们先来了解一下浏览器是如何对网页进行渲染的：</p><ol><li>浏览器将从服务器获取的HTML文档构建成文档对象模型DOM(Document Object Model).</li><li>样式将被载入和解析，构成层叠样式表模型CSSOM(CSS Object Model).</li><li>在DOM和CSSOM之上，渲染树(rendering tree)将会被创建，代表一系列将被渲染的对象（这在Webkit内核中被称为renderer或者渲染对象render object，在Gecko内核中被称为框架frame）。渲染树映射除了不可见元素（例如<strong>&lt;head&gt;</strong>或者含有<strong>display:none;</strong>的标签）外的所有DOM结构。每一段文本字符串都将划分在不同的渲染对象中，每一个渲染对象都包含了它相应的DOM对象以及计算后的样式。换句话讲，渲染树是DOM的直观表示。</li><li>渲染树的每个元素包含的内容都是计算过的，它被称之为布局layout.浏览器使用一种流式处理的方法，只需要一次pass绘制操作就可以布局所有的元素（<strong>tables</strong>需要多次pass绘制，pass表示像素处理和顶点处理）。</li><li>最后布局完成，渲染树将转化为屏幕上的实际内容，这一步被称为绘制painting.</li></ol><h2 id="-repaint">重绘Repaint</h2><p>当页面元素样式的改变不影响元素在文档流中的位置时（例如<strong>background-color, border-color,visibility</strong>）,浏览器只会将新样式赋予元素并进行重绘操作。</p><h2 id="-reflow">回流Reflow</h2><p>当改变影响文档内容或者结构，或者元素位置时，回流操作就会被触发，一般有以下几种情况：</p><ul><li>DOM操作（对元素的增删改，顺序变化等）；</li><li>内容变化，包括表单区域内的文本改变；</li><li>CSS属性的更改或重新计算；</li><li>增删样式表内容；</li><li>修改class属性；</li><li>浏览器窗口变化（滚动或缩放）；</li><li>伪类样式激活（<strong>:hover</strong>等）。</li></ul><h2 id="--1">浏览器如何优化渲染</h2><p>浏览器本身会尽可能地减少其重绘或回流的次数，只更改必要的元素。例如一个<strong>position</strong>设置为<strong>absolute/fixed</strong>的元素的更改只会影响其本身和其子元素，而<strong>static</strong>的元素变化则会影响其之后的所有页面元素。</p><p>另外一项优化的技术则是在JavaScript代码运行时，浏览器会缓存所有的变化，然后只通过一次pass绘制操作来应用这些更改。例如下面这段代码只会触发一次重绘和回流：</p><pre><code class="language-js">var $body = $('body');
$body.css('padding', '1px'); // 触发重绘与回流
$body.css('color', 'red'); // 触发重绘
$body.css('margin', '2px'); // 触发重绘与回流
// 最终只有一次重绘和回流被触发

</code></pre><p>然而，根据我们之前提到过的，获取某个元素的属性将会触发强制回流。比如我们在刚才的代码中加上一句读取元素属性的操作：</p><pre><code class="language-js">var $body = $('body');
$body.css('padding', '1px');
$body.css('padding'); // 此处触发强制回流
$body.css('color', 'red');
$body.css('margin', '2px');

</code></pre><p>结果就会有两次回流发生。因此，我们应该尽量合并读取元素属性的操作来优化性能。</p><p>当然也有我们不得不触发强制回流的情况。比如说对同一个元素的<strong>margin-left</strong>属性进行两次操作——开始的时候赋值<strong>100px</strong>的距离，之后为了实现动画效果，再加上<strong>transition</strong>属性将距离改变到<strong>50px</strong>.</p><p>我们先定义一个CSS类：</p><pre><code class="language-css">.has-transition {
   -webkit-transition: margin-left 1s ease-out;
      -moz-transition: margin-left 1s ease-out;
        -o-transition: margin-left 1s ease-out;
           transition: margin-left 1s ease-out;
}

</code></pre><p>之后再对页面元素进行操作：</p><pre><code class="language-js">// 我们的元素开始默认含有 "has-transition" 的class属性
var $targetElem = $('#targetElemId');

// 移除默认的 "has-transition"
$targetElem.removeClass('has-transition');

// 此处的属性改变没有动画效果
$targetElem.css('margin-left', 100);

// 再加上原来的属性名
$targetElem.addClass('has-transition');

// 这次改变有动画效果
$targetElem.css('margin-left', 50);

</code></pre><p>但事实上这段代码并不会像注释描述的那样运作，每条语句的操作将被缓存，只有结果会在页面上显示，所以我们就需要手动进行一次强制回流：</p><pre><code class="language-js">// 移除默认的 "has-transition"
$(this).removeClass('has-transition');

// 此处的属性改变没有动画效果
$(this).css('margin-left', 100);

// 触发强制回流，上述两条语句的效果会马上在页面中显示
$(this)[0].offsetHeight; // 只是举个例子，别的触发方法也可以

// 再加上原来的属性名
$(this).addClass('has-transition');

// 这次改变有动画效果
$(this).css('margin-left', 50);

</code></pre><p>你可以<a href="https://link.zhihu.com/?target=http%3A//output.jsbin.com/qutev/1/" rel="nofollow noreferrer">在JSBin</a>预览这个例子。</p><h2 id="--2">优化渲染效率的几条最佳实践</h2><p>根据我查阅的一些资料，总结出以下几条优化建议：</p><ul><li>合法地书写HTML和CSS，不要忘了文档编码类型。样式文件应当在<strong> &lt;head&gt; </strong>标签中，脚本文件在<strong> &lt;body&gt;</strong> 结束前。</li><li>简化并优化你的CSS选择器（有些人可能CSS预处理器用习惯了从来不关注这一点）。将嵌套层减少到最小。CSS选择器根据其优先级具有不同的运行效率（从快到慢）：</li><li>ID选择器： <strong>#id</strong></li><li>类选择器： <strong>.class</strong></li><li>标签选择器： <strong>div</strong></li><li>相邻选择器： <strong>a + i</strong></li><li>子元素选择器： <strong>ul &gt; li</strong></li><li>通用选择器： <strong>*</strong></li><li>属性选择器： <strong>input[type="text"]</strong></li><li>伪类选择器： <strong>a:hover</strong></li></ul><p>浏览器中CSS选择器是从右到左进行匹配的（<a href="https://link.zhihu.com/?target=http%3A//stackoverflow.com/questions/5797014/why-do-browsers-match-css-selectors-from-right-to-left" rel="nofollow noreferrer">为什么浏览器要从右到左匹配样式选择器</a>），这也是为什么越短的选择器运行越快的原因（别提通用选择器，它会遍历所有元素）：</p><pre><code class="language-css">div * {...} // ×
.list li {...} // ×
.list-item {...} // √
#list .list-item {...} // √

</code></pre><ul><li>在你的脚本代码中，尽量减少DOM操作。缓存所有的内容，包括属性和对象（如果他们需要被复用的话）。尽量将元素缓存到本地之后再进行操作，最后再添加到DOM当中。</li><li>如果你使用jQuery进行DOM操作的话，最好遵循<a href="https://link.zhihu.com/?target=http%3A//www.ruanyifeng.com/blog/2011/08/jquery_best_practices.html" rel="nofollow noreferrer">jQuery最佳实践</a>。</li><li>修改元素样式时，更改其<strong>class</strong>属性是性能最高的方法。你的选择器越有针对性越好（这同样也有助于分离页面样式和逻辑）。</li><li>尽量只对 <strong>position</strong> 为 <strong>absolute/fixed</strong> 的元素设置动画。</li><li>在页面滚动时禁用 <strong>:hover</strong> 样式效果：</li></ul><pre><code class="language-css">.disable-hover {
  pointer-events: none;
}

</code></pre><pre><code class="language-js">var body = document.body,
    timer;

window.addEventListener('scroll', function() {
  clearTimeout(timer);
  if(!body.classList.contains('disable-hover')) {
    body.classList.add('disable-hover')
  }
  
  timer = setTimeout(function(){
    body.classList.remove('disable-hover')
  },500);
}, false);

</code></pre><p>如果你想对此话题进行更深入的了解，可以查阅：</p><ol><li><a href="https://link.zhihu.com/?target=http%3A//www.codeweblog.com/how-browsers-work-%25E4%25B8%25AD%25E6%2596%2587%25E7%2589%2588/" rel="nofollow noreferrer">How browsers work 中文版</a></li><li><a href="https://link.zhihu.com/?target=https%3A//segmentfault.com/a/1190000006917754" rel="nofollow noreferrer">【译】浏览器渲染：repaint,reflow/relayout,restyle</a></li></ol><p>原文：<a href="http://frontendbabel.info/articles/webpage-rendering-101/">What Every Frontend Developer Should Know About Webpage Rendering</a>，作者：Alexander Skutin</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 用 C 语言来实现一个简单的网络客户端 ]]>
                </title>
                <description>
                    <![CDATA[ 当输入网址敲下回车的那一刻浏览器为我们做了什么呢？不妨今天就来看看用 C 语言怎么做到下载一个 HTML 页面的。 当我们在浏览器输入网址按下回车，浏览器向服务器发送一些数据。 GET / HTTP/1.1 Host: localhost 譬如说上面这样的，服务器端的程序接收到了之后就会做出一些回应，有可能是 HTML 页面，或者说其他的数据。 那怎么用 C 语言来写个程序发送这个数据呢？在 Unix 里面一切都是文件，每个进程都有一张表来记录打开的文件，这张表记录了文件的指针和一个数字。 那其实向网络的另一端发送数据就是往某个文件里头写数据。我们用 C 语言往文件里头写数据读数据就行了咯。我们怎么知道要往什么地方写，从什么地方读呢？ getaddrinfo() 获取域名的地址 既然要向服务器发送数据，得需要知道服务器的地址。我们需要知道 IP 地址啦，端口，协议什么的。到了今天这些当然不需要手动来填了。我们有一个函数  getaddrinfo()  ，它会帮我们填好IP，端口那些参数。 #include <sys/types.h> #include <sys/so ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/c-simple-network-client/</link>
                <guid isPermaLink="false">5e9ac078db4be8080eb70989</guid>
                
                    <category>
                        <![CDATA[ C语言 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 浏览器 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 客户端 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ 王调科 ]]>
                </dc:creator>
                <pubDate>Wed, 09 Sep 2020 09:20:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2020/04/0_ZoBttIsLXw2VVC4o.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>当输入网址敲下回车的那一刻浏览器为我们做了什么呢？不妨今天就来看看用 C 语言怎么做到下载一个 HTML 页面的。</p>
<p>当我们在浏览器输入网址按下回车，浏览器向服务器发送一些数据。</p>
<pre><code>GET / HTTP/1.1
Host: localhost
</code></pre>
<p>譬如说上面这样的，服务器端的程序接收到了之后就会做出一些回应，有可能是 HTML 页面，或者说其他的数据。</p>
<p>那怎么用 C 语言来写个程序发送这个数据呢？在 Unix 里面一切都是文件，每个进程都有一张表来记录打开的文件，这张表记录了文件的指针和一个数字。</p>
<p><img src="https://chinese.freecodecamp.org/news/content/images/2020/04/1-2.png" alt="descriptor table from &quot;head first c" width="600" height="400" loading="lazy"></p>
<p>那其实向网络的另一端发送数据就是往某个文件里头写数据。我们用 C 语言往文件里头写数据读数据就行了咯。我们怎么知道要往什么地方写，从什么地方读呢？</p>
<h2 id="getaddrinfo">getaddrinfo() 获取域名的地址</h2>
<p>既然要向服务器发送数据，得需要知道服务器的地址。我们需要知道 IP 地址啦，端口，协议什么的。到了今天这些当然不需要手动来填了。我们有一个函数 <code>getaddrinfo()</code> ，它会帮我们填好IP，端口那些参数。</p>
<pre><code>#include &lt;sys/types.h&gt;
#include &lt;sys/socket.h&gt;
#include &lt;netdb.h&gt;

int
getaddrinfo(const char *hostname,           // e.g. "www.example.com" or IP
            const char *servname,           // e.g. "http" or port number
            const struct addrinfo *hints, 
            struct addrinfo **res);
</code></pre>
<p>后面两个参数一个叫做 hints，一个叫做 res。顾名思义，我们要给它一点提示，暗示它应该怎么去拿到地址信息。然后会把回应的 response 放到 res 里面去。</p>
<p>不妨先看一个 showip 的例子。这个程序根据域名，去查询这个域名对应的 IP 地址是多少，然后我们把 IP 地址输出。当然计算机里面的东西都是二进制的，我们需要调用一些函数来帮我们把数据转换成需要的样子。函数的用法都可以用 man 命令去查询。</p>
<pre><code>#include &lt;stdio.h&gt;
#include &lt;sys/types.h&gt;
#include &lt;sys/socket.h&gt;
#include &lt;netdb.h&gt;
#include &lt;string.h&gt;
#include &lt;arpa/inet.h&gt;

int main(int argc, char const *argv[])
{
    struct addrinfo hints, *res;

    memset(&amp;hints, 0, sizeof(hints));
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;

    int status = getaddrinfo("www.example.com", "http", &amp;hints, &amp;res);

    if (status != 0) {
        fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(status));
        return 1;
    }

    char ipstr[INET6_ADDRSTRLEN];
    for (struct addrinfo *p = res; p != NULL; p = p-&gt;ai_next) {
        void *addr = NULL;
        if (p-&gt;ai_family == AF_INET) {
            struct sockaddr_in *sa = (struct sockaddr_in *)p-&gt;ai_addr;
            addr = &amp;(sa-&gt;sin_addr);
        } else {
            struct sockaddr_in6 *sa = (struct sockaddr_in6 *)p-&gt;ai_addr;
            addr = &amp;(sa-&gt;sin6_addr);
        }
        inet_ntop(p-&gt;ai_family, addr, ipstr, INET6_ADDRSTRLEN);
        printf("%s\n", ipstr);
    }
    freeaddrinfo(res);
    return 0;
}

</code></pre>
<p>getaddrinfo() 会把一个包含地址信息的链表交给 res，res 是链表的头。然后我们可以写一个循环去遍历链表，输出得到的 IP 地址。我们的重点不是这个，重点是 getaddrinfo() 是个好帮手，能让我们不必再手动填结构的数据了。</p>
<h2 id="socket">socket() 获取文件描述符</h2>
<p>socket 中文叫做套接字，在 Unix/Linux 里头其实就是一个文件描述符（file descriptor)，而所谓的文件描述符就是一个数字。套接字是双向的数据流，你可以往里头写数据，也可以从这里读数据。</p>
<pre><code>#include &lt;sys/socket.h&gt;

int
socket(int domain, int type, int protocol);
</code></pre>
<p>我们用 getaddrinfo() 得到的 res 来填这三个参数就好了。譬如可能是这样的。</p>
<pre><code>int s;
struct addrinfo hints, *res;

getaddrinfo("www.example.com", "http", &amp;hints, &amp;res);

s = socket(res-&gt;ai_family, res-&gt;ai_socktype, res-&gt;ai_protocol);
</code></pre>
<p>socket() 返回的是一个 int，这个数字就是文件描述符。得到了文件描述符那就能对文件描述符所代表的数据流写数据了。</p>
<h2 id="connect">connect() 把套接字连接到远程端口</h2>
<pre><code>#include &lt;sys/types.h&gt;
#include &lt;sys/socket.h&gt;

int
connect(int socket, const struct sockaddr *address, socklen_t address_len);
</code></pre>
<p>把套接字连接到远程端口我们就可以对套接字读数据写数据了。</p>
<pre><code>struct addrinfo hints, *res;
int sockfd;

// 首先用 getaddrinfo() 加载地址的结构

memset(&amp;hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;

getaddrinfo("www.example.com", "http", &amp;hints, &amp;res);

// 套接字

sockfd = socket(res-&gt;ai_family, res-&gt;ai_socktype, res-&gt;ai_protocol);

// 连接!

connect(sockfd, res-&gt;ai_addr, res-&gt;ai_addrlen);
</code></pre>
<p>connect() 第一个参数是 socket() 返回的那个文件描述符，然后后面的 address 和 address_len 用 res 里头的成员来填。</p>
<h2 id="sendrecv">send() 和 recv()</h2>
<p>有了 socket 就要用 send() 和 recv() 来发送数据和接收数据了。socket 是一个文件描述符，既然是一个文件描述符那为什么不直接用 read() 和 write() 呢？当然可以用，但是 send() 和 recv() 能更好的控制数据传输。</p>
<pre><code>#include &lt;sys/socket.h&gt;

ssize_t
send(int socket, const void *buffer, size_t length, int flags);

ssize_t
recv(int socket, void *buffer, size_t length, int flags);
</code></pre>
<p>连上网络服务器后至少需要发送三样东西</p>
<pre><code>GET / HTTP/1.0
Host: www.example.com

</code></pre>
<p>最后有个空行。那来看看最终的程序的样子吧。</p>
<pre><code>#include &lt;stdio.h&gt;
#include &lt;sys/types.h&gt;
#include &lt;sys/socket.h&gt;
#include &lt;netdb.h&gt;
#include &lt;string.h&gt;
#include &lt;unistd.h&gt;

int main(int argc, char const *argv[])
{
    struct addrinfo hints, *res;
    int sockfd, recvd;

    memset(&amp;hints, 0, sizeof hints);
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;

    getaddrinfo("www.example.org", "http", &amp;hints, &amp;res);

    sockfd = socket(res-&gt;ai_family, res-&gt;ai_socktype, res-&gt;ai_protocol);
    connect(sockfd, (struct sockaddr *)res-&gt;ai_addr, res-&gt;ai_addrlen);

    send(sockfd, "GET / HTTP/1.0\r\n"
                 "Host: www.example.com\r\n\r\n", 47, 0);

    char buffer[1024];
    while ((recvd = recv(sockfd, buffer, sizeof(buffer) -1, 0)) != 0) {
        buffer[recvd] = '\0';
        printf("%s", buffer);
    }
    freeaddrinfo(res);
    close(sockfd);
    return 0;
}
</code></pre>
<p>send() 函数的第二个参数就是要发送的数据，每行以 <code>\r\n</code> 结尾，意思就是回车换行。recv() 函数可能不能一次性接收完所有的数据，所以我们应该写个循环来不断的从网络上接收数据。recv() 函数会返回接收了多少个字节的数据，在接收的数据后面加个 <code>\0</code> 让它成为一个 C 语言的字符串，然后用 printf() 输出。</p>
<p>必须记得要检查是否发生错误，这里没处理只是想让这个程序看起来不那么可怕。下面是处理了错误的版本。</p>
<pre><code>#include &lt;stdio.h&gt;
#include &lt;sys/types.h&gt;
#include &lt;sys/socket.h&gt;
#include &lt;netdb.h&gt;
#include &lt;string.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;unistd.h&gt;

int main(int argc, char const *argv[])
{
    struct addrinfo hints, *p, *res;
    int sockfd, recvd, status;

    memset(&amp;hints, 0, sizeof hints);
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;

    if ((status = getaddrinfo("www.example.org", "http", &amp;hints, &amp;res)) != 0) {
        fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(status));
        exit(1);
    }

    for (p = res; p != NULL; p = p-&gt;ai_next) {
        sockfd = socket(p-&gt;ai_family, p-&gt;ai_socktype, p-&gt;ai_protocol);
        if (sockfd == -1) {
            perror("socket");
            continue;
        }
        if (connect(sockfd, (struct sockaddr *)p-&gt;ai_addr, p-&gt;ai_addrlen) == -1) {
            perror("connect failed. retrying...");
            continue;
        }
        break;
    }
    freeaddrinfo(res);

    if (p == NULL) {
        fprintf(stderr, "failed!");
        exit(1);
    }

    if (send(sockfd, "GET / HTTP/1.0\r\n"
                 "Host: www.example.com\r\n\r\n", 47, 0) == -1) {
        perror("send");
        exit(1);
    }

    char buffer[1024];
    while ((recvd = recv(sockfd, buffer, sizeof(buffer) -1, 0)) != 0) {
        buffer[recvd] = '\0';
        printf("%s", buffer);
    }
    close(sockfd);
    return 0;
}

</code></pre>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ about:blank 是什么意思？ ]]>
                </title>
                <description>
                    <![CDATA[ 你有没有尝试过访问网页，在本应该输入 URL 的地址栏中看到 “about:blank”？ 不用担心，有时会出现这种情况，没有大问题。这篇文章会介绍：  * about:blank 是什么意思  * 它为什么会出现  * 为什么你可能要使用它  * 是否可以避免它，以及  * 你是否需要担心它 我们开始吧！ about:blank 是什么意思 about:blank 是当浏览器无其他显示时显示的页面。它不是互联网上的页面，而是浏览器内部的内容。 你所看到的 “about” 部分来自浏览器的 about URI 或 URL 方案 [https://en.wikipedia.org/wiki/About_URI_scheme#:~:text=about%20is%20an%20internal%20URI,registered%20scheme%2C%20and%20is%20standardized.&text=An%20exception%20is%20about%3Ablank%20%2C%20which%20is%20not%20translated.] 。你可以在地址栏 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/about-blank-what-does-about-blank-mean-and-why-is-it-blocked-in-chrome-and-firefox/</link>
                <guid isPermaLink="false">6029fc386183a705401568be</guid>
                
                    <category>
                        <![CDATA[ 浏览器 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Chengjun.L ]]>
                </dc:creator>
                <pubDate>Tue, 01 Sep 2020 07:20:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/02/photo-1501618669935-18b6ecb13d6d.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>你有没有尝试过访问网页，在本应该输入 URL 的地址栏中看到 “about:blank”？</p><p>不用担心，有时会出现这种情况，没有大问题。这篇文章会介绍：</p><ul><li>about:blank 是什么意思</li><li>它为什么会出现</li><li>为什么你可能要使用它</li><li>是否可以避免它，以及</li><li>你是否需要担心它</li></ul><p>我们开始吧！</p><h2 id="about-blank-"><strong><strong>about:blank</strong> 是什么意思</strong></h2><p>about:blank 是当浏览器无其他显示时显示的页面。它不是互联网上的页面，而是浏览器内部的内容。</p><p>你所看到的 “about” 部分来自浏览器的 <a href="https://en.wikipedia.org/wiki/About_URI_scheme#:~:text=about%20is%20an%20internal%20URI,registered%20scheme%2C%20and%20is%20standardized.&amp;text=An%20exception%20is%20about%3Ablank%20%2C%20which%20is%20not%20translated.">about URI 或 URL 方案</a>。你可以在地址栏中键入 “about:[任何内容]”以了解有关浏览器的更多信息。</p><p>这适用于大多数主流浏览器，例如 Chrome、Firefox、Safari、Edge、Chromium、 Internet Explorer 等。</p><p>例如，在 Chrome 中，你可以输入 “about:dino”（Chrome 将其更改为 chrome：// dino），然后你会看到 Chrome 中有名的 “No Internet” 情况下的 dino 消息：</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://www.freecodecamp.org/news/content/images/2020/08/about-dino.png" class="kg-image" alt="about-dino" width="600" height="400" loading="lazy"></figure><p>如果你从未玩过这个恐龙游戏，可以<a href="https://chinese.freecodecamp.org/news/do-you-know-the-chrome-dino-game-millions-of-people-are-playing/">在这里学习如何操作</a>（可能你会玩上瘾哦）。</p><p>更有用的是，例如，“about:about” 显示Chrome URL 的列表：</p><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="https://www.freecodecamp.org/news/content/images/2020/08/about-about-chrome.png" class="kg-image" alt="about-about-chrome" width="600" height="400" loading="lazy"><figcaption>截图只显示部分 URL</figcaption></figure><p>about:blank 的 “:blank” 部分是告诉你的浏览器在没有其他显示时抛出空白页。</p><p>因此，它实际上只是向你显示空白的页面，上面没有任何内容：</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://www.freecodecamp.org/news/content/images/2020/08/about-blank.png" class="kg-image" alt="about-blank" width="600" height="400" loading="lazy"></figure><p>再说一次，这不是互联网上的某个位置，这是浏览器为此目的而保留的空白页。</p><h2 id="about-blank--1">about:blank 为什么会出现</h2><p>那么为什么有时会出现空白页呢？ 一些主要原因是：</p><ul><li>如果你遇到错误的链接/URL</li><li>如果浏览器不知道跳转到哪里，但它必须做一些事情，这时它会向你显示 about:blank</li><li>如果你的浏览器检测到危险内容（例如恶意软件），则某些浏览器将显示 about:blank，而不是进入危险区域</li></ul><p>正如你所看到的，并不需要担心 about:blank 本身。但是，如果你经常看到它，则可能要检查计算机上是否存在恶意软件。下面介绍更多内容。</p><h2 id="-about-blank"><strong><strong>如何使用</strong> <strong>about:blank</strong></strong></h2><p>现在你可能想知道——为什么要故意出现这个空白页？ 这似乎很没用。</p><p>如果带宽有限或者互联网连接缓慢，则可以将浏览器的首页设置为 “about:blank”。这样，无论何时打开浏览器或新选项卡，它都将快速加载，而不会浪费资源或时间。</p><h3 id="-about-blank-">如何将 about:blank 设置为主页</h3><p>在 Chrome 中，打开菜单（浏览器右上角的三个垂直小点），然后向下滚动到“启动时”部分（可能在底部）：</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/2020/08/make-about-blank-homepage-1.png" class="kg-image" alt="make-about-blank-homepage-1" width="600" height="400" loading="lazy"></figure><p>然后选择“打开特定网页或一组网页”（第三个选项），并在出现的文本字段中键入“about:blank”。</p><p>然后，当你打开浏览器或新选项卡时，应该会看到一个空白的白页。</p><h3 id="about-blank--2">about:blank 的其他用途</h3><p>你可能还需要确保在打开浏览器时，没有保留任何标签或上次浏览会话中的任何内容。</p><p>这对保护隐私很有帮助，并且还允许你在没有网络的情况下即可启动浏览会话。</p><h2 id="-about-blank--1"><strong><strong>你可以避免</strong> <strong>about:blank</strong> 吗</strong></h2><p>about:blank 并不是需要“摆脱”或避免的东西。但是，如果你每次打开浏览器都看到它，则可能是因为设置了在启动时显示 about:blank。</p><p>如果你不希望发生这种情况，只需并将主页更改为你所需的页面即可（按照上述步骤）。</p><h2 id="about-blank--3"><strong><strong>about:blank</strong> 是恶意软件或病毒吗</strong></h2><p>如前文所述，about:blank 本身并不是恶意软件，但它可能表明你的计算机正在阻止恶意软件。因此它和恶意软件可能是有关联的。</p><p>某些防病毒和防恶意软件在检测到错误的 URL 或站点时会使用 about:blank。浏览器会抛出空白页来保护你，而不是将你定向到该恶意网站。</p><p>从系统中删除恶意软件之后，也会显示 about:blank 页面。如果在删除过程中出现问题，浏览器将显示 about:blank，因为它无法完成你要求它执行的操作。</p><p>总之，除非你希望将 about:blank 设置为主页，或者认为你需要检查计算机上可能存在的恶意软件或病毒，否则你无需担心 about:blank。</p><p>原文：<a href="https://www.freecodecamp.org/news/about-blank-what-does-about-blank-mean-and-why-is-it-blocked-in-chrome-and-firefox/">About Blank – What Does about:blank Mean and Should You Get Rid of It?</a>，作者：<a href="https://www.freecodecamp.org/news/author/abbey/">Abbey Rennemeyer</a><br></p> ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
