快速且全面了解浏览器执行原理

前情提要:本文章适用于那些初学者学习前端,为了不影响后面的知识学习,想要快速且完整了解浏览器执行原理的同学🚀,全篇读完大约十分钟😄

如果想要深入了解,可以看我这篇文章,里面对此进行了详细说明🏃‍♀️

网页的解析过程

大家有没有深入思考过:一个网页URL从输入到浏览器中,到显示经历过怎么样的解析过程呢?🤔 我们这里简单概述一下

  1. URL 解析:将输入的 URL 分解为协议、域名、路径等部分。

  2. DNS 解析:将域名解析为服务器的 IP 地址。

  3. 建立连接

    • 使用 TCP 完成三次握手。
    • 若为 HTTPS,还需进行 TLS 加密握手。

4.发送 HTTP 请求:浏览器向服务器发送请求(如 GET)。

5.服务器响应:服务器处理请求并返回 HTML、CSS、JS 等资源。

6.内容解析与渲染

diff 复制代码
-   浏览器解析 HTML,构建 DOM 树。
-   解析 CSS,生成样式表。
-   执行 JavaScript,动态修改 DOM。
-   进行布局计算和绘制。

7.资源加载:并行请求其他资源(如图片、视频、额外脚本)。

8.页面显示与交互:渲染完成后用户可操作,浏览器持续处理更新。

接下来让我们看看下面这张图,可以大概了解浏览器在构建我们页面的流程

可以总结出来浏览器在构建我们页面的大致流程为

  • 输入服务器地址:用户在浏览器中输入 URL,浏览器向服务器发起请求。
  • 返回 HTML 文件 :服务器响应请求,将主页面文件(例如 index.html)返回给浏览器。
  • 解析 HTML 文件
    • 浏览器开始解析 HTML 文档,当遇到特定的资源引用标签时,会发起额外请求:
    • <link> 元素:遇到link元素,就回去解析link元素里面的内容
    • <script> 元素:遇到script标签就会解析js文件
  • 加载 CSS 文件 : 浏览器解析 link 标签后,向服务器请求 CSS 文件(如 xxx.css),以加载页面样式。
  • 加载 JavaScript 文件 :浏览器解析 script 标签后,向服务器请求 JavaScript 文件(如 xxx.js),用于动态行为。
  • 资源下载完成:CSS 和 JavaScript 文件分别返回给浏览器,并与 HTML 结合,最终渲染完整页面。

注意⚠️:在我们部署静态资源的时候,一般情况在下载下来的都是html,文件,如果碰到link元素,再去解析css,碰到script元素再去下载js文件

浏览器内核

想对浏览器内核进行了解的,可以看这篇文章➡️

渲染引擎如何解析页面呢?

渲染引擎在拿到一个页面后,如何解析整个页面并且最终呈现出我们的网页呢?🤔

我们对下面这张图进行分析,这张图请你刻在脑子里面😡

1. HTML 文件的解析

  • 输入:HTML 文件。

  • 过程 :通过 HTML Parser (HTML 解析器)解析 HTML,生成 DOM 树(Document Object Model)。

    • DOM 树是一种树状结构,表示 HTML 文档的逻辑结构。

2. CSS 文件的解析

  • 输入:CSS 样式表。

  • 过程 :通过 CSS Parser (CSS 解析器)解析 CSS,生成 Style Rules(样式规则)。

    • 这些样式规则包含了页面元素的外观定义(颜色、大小、布局等)。

3. DOM 树与样式的结合

  • 过程 :将解析得到的 DOM 树Style Rules 进行关联,形成 Attachment(附加)。
  • 结果:在 DOM 树的基础上,将样式应用到每个对应的节点。

4. 生成 Render Tree(渲染树)

  • 过程 :根据 DOM 树和样式规则,生成 Render Tree

    • 渲染树是为显示页面内容而创建的树结构,包含页面可见元素及其样式信息。
    • 不可见元素(如 display: none)不会出现在渲染树中。

5. 布局(Layout)

  • 输入:渲染树。

  • 过程:根据渲染树计算每个元素的位置和大小。

    • 布局阶段负责确定页面上每个元素的几何信息(即页面布局)。

6. 绘制(Painting)

  • 输入:经过布局计算的渲染树。
  • 过程:将每个元素的样式和几何信息转换为实际像素,绘制到屏幕的绘图缓冲区。

7. 显示(Display)

  • 过程:浏览器将绘制好的内容展示在屏幕上,完成整个渲染流程。

接下来我们进行详细分析每个阶段

解析一:解析html

因为默认情况下服务器会给浏览器返回index.html文件,所以解析HTML是所有步骤的开始:

解析HTML,会构建DOM Tree:

解析二:解析css

  • 在解析的过程中,如果遇到CSS的link元素,那么会由浏览器负责下载对应的CSS文件:

  • 注意:下载CSS文件是不会影响DOM的解析的;

  • 浏览器下载完CSS文件后,就会对CSS文件进行解析,解析出对应的规则树:

  • 我们可以称之为 CSSOM(CSS Object Model,CSS对象模型);

注意:解析html和css都是有自己的线程,是不会相互阻塞的,不会影响DOMtree生产过程,

为什么呢?🤔

1. DOM 和 CSSOM 是独立的解析过程

  • DOM 的解析 :浏览器会从上到下解析 HTML 文件,构建 DOM 树。这是一个流式操作,边解析边构建 DOM。

  • CSSOM 的解析 :当遇到 <link> 标签时,浏览器会异步请求对应的 CSS 文件,并在收到后开始解析它,生成 CSSOM。

  • 这两个过程是独立的:

    • 浏览器可以继续解析 HTML 文档,构建 DOM 树。
    • 不需要等待 CSS 文件加载完成或解析完成才继续 DOM 的解析。

2. 解析过程是并行的

  • 浏览器具有并行能力 ,允许 HTML 的解析CSS 的下载 同时进行。
  • 在遇到 <link> 标签时,浏览器会将 CSS 文件的下载任务交给网络线程 ,而 DOM 的解析继续在主线程中执行。
  • 这种机制确保了解析 HTML 的效率,不会因为外部资源加载而阻塞

还有一点注意⚠️,虽然他们两个不会互相影响,但是他们一般都会影响RenderTree的生成(但是也不一定,具体看浏览器的优化)

3. 关键路径优化(Critical Rendering Path Optimization)

  • 关键渲染路径:包括构建 DOM 树、CSSOM 和渲染树的核心部分。

  • 浏览器会优先下载和解析被标记为 "关键资源" 的 CSS 文件。

    • 如果 CSS 文件体积较小且加载较快,浏览器可以优先完成其 CSSOM 的生成。
    • 非关键的 CSS(如 media 查询不匹配的样式)可能被延迟加载或解析。
4. 部分渲染
  • 对于未受样式影响的 DOM 节点,浏览器可能会 提前构建部分 Render Tree 并渲染

    • 例如,对于 HTML 文件中靠前的元素,可能先渲染无样式版本,然后在 CSSOM 构建完成后再应用样式重新渲染。
    • 这种方式可以减少页面的白屏时间。
5. 渐进渲染(Progressive Rendering)
  • 浏览器可能分段处理 DOM 树和 CSSOM,例如:

    • 构建初步的 Render Tree,渲染内容。
    • 待后续 CSS 文件完成加载后,再更新渲染树并重新渲染。
  • 这种优化可以显著提升用户的感知性能。

6. 异步或非阻塞加载
  • 非阻塞的 CSS 文件加载 :通过 <link> 标签的 media 属性或动态加载的 CSS 文件,浏览器可能会延迟加载某些非关键的样式,优先渲染重要内容

构建Render Tree

当有了DOM Tree和 CSSOM Tree后,就可以两个结合来构建Render Tree了

  • 注意一:link元素不会阻塞DOM Tree的构建过程,但是会阻塞Render Tree的构建过程
  • 注意二:Render Tree和DOM Tree并不是一一对应的关系,比如对于display为none的元素,压根不会出现在render tree中;

注意 1.默认情况下Rendertree上的节点是没有每个节点大小和位置信息,只有通过layout解析后,才会被解析出来。

  1. DOM Tree和Render Tree不一定是一一对应的。

这是为什么呢🤔?

1.默认情况下Rendertree上的节点是没有每个节点大小和位置信息 ,只有通过layout 解析后,才会被解析出来

为什么呢?🤔

Render Tree 的初步生成 仅仅是将 DOM 节点样式规则 (Style Rules)结合,表示哪些元素需要被绘制以及它们的基本样式信息。- 元素的具体几何信息(大小和位置)只有在 Layout 阶段 才会通过计算得出。这是因为,布局阶段会综合考虑多种因素,如:

  • CSS 布局模型(如 box model)。
  • 元素的父子关系和依赖(例如,子元素的大小依赖于父元素的大小)。
  • 浏览器窗口的尺寸(特别是响应式布局)。

因此,只有经过 Layout 阶段 的几何计算后,Render Tree 上的节点才会获得大小和位置信息。在这之前,渲染树中的节点仅表示页面的结构和样式逻辑。

  1. DOM Tree和Render Tree不一定是一一对应的

这是为什么呢?🤔

1. 不可见的元素不进入 Render Tree

某些 DOM 节点虽然存在于文档中,但由于样式规则或者 HTML 属性的影响,并不会被绘制。例如:

  • 元素被设置为 display: none

    • 这类元素完全从 Render Tree 中排除,因为它们既不可见,也不占据空间。
  • 注释节点、<script><meta> 等非渲染内容的节点:

    • 这些 DOM 节点不会参与页面渲染,因此不再 Render Tree 中出现。
2. 伪元素的引入
  • Render Tree 中可能包含 CSS 伪元素 (如 ::before::after),这些伪元素并不存在于 DOM Tree 中,但它们会被渲染引擎加入 Render Tree,表示需要绘制的内容。
3. 某些节点被拆分为多个渲染节点
  • 一个 DOM 节点可能对应多个 Render Tree 节点。例如:

    • 如果一个元素有多种样式分段(如文本内容中有部分文字用 span 包裹并应用了不同的样式),它可能在 Render Tree 中被分解成多个节点。
    • 例如,带有复杂 CSS 样式的 <p> 元素可能在 Render Tree 中被拆分为多个部分。
4. 不同的渲染模型
  • 一些复杂布局(如表格布局)会在 Render Tree 中使用额外的辅助节点来表示,甚至可能重组部分节点关系,这使得 DOM 和 Render Tree 的映射更加复杂。

解析四 -- 布局(layout)和绘制(Paint)

第四步是在渲染树(Render Tree)上运行布局(Layout)以计算每个节点的几何体。

  • 渲染树会表示显示哪些节点以及其他样式,但是不表示每个节点的尺寸、位置等信息;

  • 布局是确定呈现树中所有节点的宽度、高度和位置信息;

  • 第五步是将每个节点绘制(Paint)到屏幕上

  • 在绘制阶段,浏览器将布局阶段计算的每个frame转为屏幕上实际的像素点;

  • 包括将元素的可见部分进行绘制,比如文本、颜色、边框、阴影、替换元素(比如img)

如下图所示

回流和重绘

回流

理解回流reflow:(也可以称之为重排)

  • 第一次 确定节点的大小和位置,称之为布局(layout)

  • 之后 对节点的大小、位置修改重新计算 称之为回流

这里我们看代码,将它运行到浏览器

js 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <link rel="stylesheet" href="./css/style.css">
</head>
<body>
  
  <div class="box">box盒子</div>
  <button>修改box</button>
  <script>

    var boxEl = document.querySelector(".box")
    var btnEl = document.querySelector("button")
    btnEl.onclick = function() {
      boxEl.remove()
      boxEl.style.height = "200px"
    }
  </script>
</body>
</html>
css 复制代码
body {
  padding: 0;
  margin: 0;
}

.box {
  width: 100px;
  height: 100px;
  background-color: orange;
  font-size: 15px;
  color: red;
}

当我们点击按钮的时候发现盒子的大小改变了,但是按钮的位置也变化了,也就是整个布局发生变化。这种情况就是回流

什么情况下引起回流呢?

比如DOM结构发生改变:(添加新的节点appendChild() 或 或者移除节点removeChild());

动态修改 DOM 结构 :例如,通过 innerHTMLdocument.createElement() 插入新的 HTML 元素。

js 复制代码
const element = document.createElement('div'); 
document.body.appendChild(element); // 添加元素,触发回流
  • 比如改变了布局(修改了width、height、padding、font-size,top,position等值)
js 复制代码
const element = document.getElementById('box');
element.style.width = '200px'; // 改变宽度,触发回流
改变页面整体布局(修改了窗口的尺寸等)

浏览器窗口大小变化:用户调整窗口大小会触发回流,因为需要重新计算布局。

滚动操作:滚动条的移动可能触发页面部分元素的重新布局

获取需要布局计算的属性

  • 比如调用getComputedStyle方法获取尺寸、位置信息;(注意,这个不一定,如果只是回去信息,不进行额外操作有些浏览器不会引起回流,具体看浏览器的优化)

改变内容(文字或图片) 动态修改文字内容或加载新图片会影响元素的尺寸,从而触发回流。 动画和过渡效果

  • 使用 CSS 动画或过渡(transition)可能涉及布局属性的变化,比如 widthheighttopleft

  • 如果动画过程中频繁更改布局相关属性,会导致多次回流。
    表格相关的操作 表格的布局(如 <table> 元素)比普通的块级元素更复杂,任何涉及表格内容或结构的变化通常都会引起回流。

重绘repaint:

是指浏览器重新绘制页面元素的外观(如颜色、边框、背景等)的过程,但不涉及布局的重新计算(即元素的大小和位置未改变)

  • 第一次渲染内容称之为绘制(paint)。

  • 之后重新渲染称之为重绘。

什么情况下会引起重绘呢?

比如修改背景色、文字颜色、边框颜色、样式,阴影,透明度等,总而言就是样式属性,而这些属性不会影响元素的几何形状或文档的布局;

总结

回流一定会引起重绘,所以回流是一件很消耗性能的事情, 所以在开发中要尽量避免发生回流:

如何避免回流?

  • 1.修改样式时尽量一次性修改 比如通过cssText修改,比如通过添加class修改

  • 2.尽量避免频繁的操作DOM,我们可以在一个DocumentFragment或者父元素中

  • 将要操作的DOM操作完成,再一次性的操作;

  • 3.尽量避免通过getComputedStyle获取尺寸、位置等信息;

  • 4.对某些元素使用position的absolute或者fixed,但这种方法并不是不会引起回流 ,而是将这些元素从 文档流 中移除后,只会影响它自身的布局 ,而不会触发整个页面的重新布局不会对其他元素造成影响,开销较小。 这就是后面说到的composite了。

总结

特殊解析-composite合成

绘制的过程,可以将布局后的元素绘制到多个合成图层中,这是浏览器的一种优化手段。

  • 默认情况下,标准流中的内容都是被绘制在同一个图层(Layer)中的;

标准流就是一个网页内的标签按照规定好的默认方式,从上到下、从左到右顺序排列。

块级元素会独占一行,从上到下顺序排列

常用元素:div、hr、p、h1~h6、ul、ol、dl、form、table

行内元素会按照顺序,从左到右顺序排列,碰到父元素自动换行

常用元素:span、a、i、em等。

  • 而一些特殊的属性,会创建一个新的合成层( CompositingLayer ),并且新的图层可以利用GPU来加速绘制, 因为每个合成层都是单独渲染的;

那么哪些属性可以形成新的合成层呢?

常见的一些属性:

  • 3D transforms

  • video、canvas、iframe

  • opacity 动画转换时;

  • position: fixed

  • will-change:一个实验性的属性,提前告诉浏览器元素可能发生哪些变化;

  • animation 或 transition 设置了opacity、transform;

js 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    .box, .container {
      width: 200px;
      height: 200px;
      background-color: orange;
      /* will-change: transform; */
    }

    .container {
      background-color: red;

      /* 会生成新的图层 */
      /* position: fixed; */
      /* transform: translateZ(0); */

      /* opacity: 0.9;
      transition: all 1s ease; */

      will-change: transform;
    }

    /* .container:hover { */
      /* transform: translateX(100px); */
      /* opacity: 0.2; */
      /* margin-left: 100px; */
    /* } */
  </style>
</head>
<body>
  
  <div class="box"></div>
  <div class="container"></div>

</body>
</html>

结果 省下的属性你们可以自己解除注释,自己玩玩😄

总结

分层确实可以提高性能,但是它以内存管理为代价,因此不应作为 web 性能优化策略的一部分过度使用

script元素和页面解析的关系

我们现在已经知道了页面的渲染过程,但是JavaScript在哪里呢?思考

  • 事实上,浏览器在解析HTML的过程中,遇到了script元素是不能继续构建DOM树的;
  • 它会停止 继续构建,首先下载JavaScript代码,并且执行JavaScript的脚本;
  • 只有等到JavaScript脚本执行结束 后,才会继续解析HTML,构建DOM树

为什么要这样做呢?🤔

  • 这是因为JavaScript的作用之一就是操作DOM,并且可以修改DOM;
  • 如果我们等到DOM树构建完成并且渲染再执行JavaScript,会造成严重的回流和重绘,影响页面的性能
  • 所以会在遇到script元素时,优先下载和执行JavaScript代码,再继续构建DOM树;

但是这个也往往会带来新的问题,特别是现代页面开发中:

  • 在目前的开发模式中(比如Vue、React),脚本往往比HTML页面更"重",处理时间需要更长
  • 所以会造成页面的解析阻塞,在脚本下载、执行完成之前,用户在界面上什么都看不到;

为了解决这个问题,script元素给我们提供了两个属性(attribute):defer和async。

defer和async

defer

  • defer 属性告诉浏览器不要等待脚本下载,而继续解析HTML,构建DOM Tree。
  • 脚本会由浏览器来进行下载,但是不会阻塞DOM Tree的构建过程;
  • 如果脚本提前下载好了,它会等待DOM Tree构建完成 ,在DOMContentLoaded事件之前先执行defer中的代码, 所以DOMContentLoaded总是会等待defer中的代码先执行完成。
  • 另外多个带defer的脚本是可以保持正确的顺序执行的。
  • 从某种角度来说,defer可以提高页面的性能,并且推荐放到head元素中;

注意:defer仅适用于外部脚本,对于script默认内容会被忽略 即这种形式,为外部脚本

js 复制代码
<script src="script.js"></script>

对于内联脚本(直接在<script>标签内写代码)

js 复制代码
<script>console.log('Hello');</script>

defer属性会被忽略。

让我们看下面的代码具体感受下defer

js 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">app</div>
  <div class="box"></div>
  <div id="title">title</div>
  <div id="nav">nav</div>
  <div id="product">product</div>
  <--注意力放在这段代码上⚠️⚠️-->
  <script src="./js/test.js"></script>
  <h1>哈哈哈哈啊</h1>
</body>
</html>
js 复制代码
console.log("test")
console.log("defer script")

var message = "test message"

//这里打上debugger,阻止继续运行
debugger

结果我们发现程序已经停在debugger的位置,可以发现哈哈哈哈啊 这段文字并没有显示出来,阻止了html解析

好,现在我们给script赋予defer属性

js 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">app</div>
  <div class="box"></div>
  <div id="title">title</div>
  <div id="nav">nav</div>
  <div id="product">product</div>
  <script src="./js/test.js" defer></script>
  <h1>哈哈哈哈啊</h1>
</body>
</html>

结果发现,即使还是停在了debugger上,但是不影响html的解析

接下来我们看这串代码

js 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  
  <div id="app">app</div>
  <div class="box"></div>
  <div id="title">title</div>
  <div id="nav">nav</div>
  <div id="product">product</div>

  <!-- 1.下载需要很长的事件, 并且执行也需要很长的时间 -->
  <!-- 总结一: 加上defer之后, js文件的下载和执行, 不会影响后面的DOM Tree的构建 -->
   <script src="./js/test.js" defer></script>
  <script>
    // 总结三: defer代码是在DOMContentLoaded事件发出之前执行
    window.addEventListener("DOMContentLoaded", () => {
      console.log("DOMContentLoaded")
    })
  </script>
  <h1>哈哈哈哈啊</h1>
</body>
</html>
js 复制代码
// console.log("test")
console.log("defer script")

// var message = "test message"

// debugger

// 总结二: 在defer代码中DOM Tree已经构建完成
var boxEl = document.querySelector(".box")
console.log(boxEl)

我们在这段结果中可以总结三点出来:

  • 总结一: 加上defer之后, js文件的下载和执行, 不会影响后面的DOM Tree的构建
  • 总结二: 在defer代码中DOM Tree已经构建完成
  • 总结三: 如果脚本先执行完,那么defer代码是在DOMContentLoaded事件发出之前执行

我们再改进一下代码

js 复制代码
console.log("test")
console.log("defer script")

var message = "test message"
var boxEl = document.querySelector(".box")
console.log(boxEl)
js 复制代码
console.log("demo")
console.log(message)

变成

js 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="./js/test.js" defer></script>
  <script src="./js/demo.js" defer></script>
</head>
<body>
  
  <div id="app">app</div>
  <div class="box"></div>
  <div id="title">title</div>
  <div id="nav">nav</div>
  <div id="product">product</div>

  <script>
    window.addEventListener("DOMContentLoaded", () => {
      console.log("DOMContentLoaded")
    })
  </script>
  <h1>哈哈哈哈啊</h1>
</body>
</html>

可以总结出来defer解析script是有顺序的

先执行text.js,然后再执行demo.js,不然text message,是无法打印出来的

async

  • async 特性与 defer 有些类似,它也能够让脚本不阻塞页面。
  • async是让一个脚本完全独立的
  • 浏览器不会因 async 脚本而阻塞(与 defer 类似);
  • async脚本不能保证顺序,它是独立下载、独立运行,不会等待其他脚本,很是野蛮啊😡;
  • async不会保证在DOMContentLoaded之前或者之后执行

这里要强调async慎用。他在我眼中是很野蛮的,在能用defer的情况在就用defer,少用async,因为他不能不能保证顺序。 看下面的代码

js 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="./js/test.js" async></script>
  <script src="./js/demo.js" async></script>
</head>
<body>

  <div id="app">app</div>
  <div id="title">title</div>
  <div id="nav">nav</div>
  <div id="product">product</div>

  <h1>哈哈哈哈啊</h1>
  <div class="box"></div>

</body>
</html>

你可以多刷新几次,有时候会出现这种情况。

建议

  • defer通常用于需要在文档解析后操作DOM的JavaScript代码,并且对多个script文件有顺序要求的;
  • async通常用于独立的脚本,对其他脚本,甚至DOM没有依赖的;
相关推荐
前端小小王34 分钟前
React Hooks
前端·javascript·react.js
迷途小码农零零发44 分钟前
react中使用ResizeObserver来观察元素的size变化
前端·javascript·react.js
娃哈哈哈哈呀1 小时前
vue中的css深度选择器v-deep 配合!important
前端·css·vue.js
旭东怪2 小时前
EasyPoi 使用$fe:模板语法生成Word动态行
java·前端·word
ekskef_sef3 小时前
32岁前端干了8年,是继续做前端开发,还是转其它工作
前端
sunshine6414 小时前
【CSS】实现tag选中对钩样式
前端·css·css3
真滴book理喻4 小时前
Vue(四)
前端·javascript·vue.js
蜜獾云4 小时前
npm淘宝镜像
前端·npm·node.js
dz88i84 小时前
修改npm镜像源
前端·npm·node.js
Jiaberrr4 小时前
解锁 GitBook 的奥秘:从入门到精通之旅
前端·gitbook