普通视图

发现新文章,点击刷新页面。
今天 — 2025年7月7日技术

每日一题-最多可以参加的会议数目🟡

2025年7月7日 00:00

给你一个数组 events,其中 events[i] = [startDayi, endDayi] ,表示会议 i 开始于 startDayi ,结束于 endDayi 。

你可以在满足 startDayi <= d <= endDayi 中的任意一天 d 参加会议 i 。在任意一天 d 中只能参加一场会议。

请你返回你可以参加的 最大 会议数目。

 

示例 1:

输入:events = [[1,2],[2,3],[3,4]]
输出:3
解释:你可以参加所有的三个会议。
安排会议的一种方案如上图。
第 1 天参加第一个会议。
第 2 天参加第二个会议。
第 3 天参加第三个会议。

示例 2:

输入:events= [[1,2],[2,3],[3,4],[1,2]]
输出:4

 

提示:

  • 1 <= events.length <= 105
  • events[i].length == 2
  • 1 <= startDayi <= endDayi <= 105

Babel 完全上手指南:从零开始解锁现代 JavaScript 开发的超能力!

作者 烛阴
2025年7月6日 22:18

引言

Babel 是现代 JavaScript 开发中不可或缺的工具链核心,它让开发者能够使用最前沿的 JavaScript 特性,同时确保代码能在各种浏览器和环境中运行。

一、理解核心概念

1.1 什么是 Babel?

Babel 是一个 JavaScript 编译器(更准确说是转译器),主要功能包括:

  • 将 ES6+ 代码转换为向后兼容的 JavaScript 版本
  • 转换 JSX 语法
  • 添加缺失的 polyfill
  • 源代码转换(codemods)

1.2 为什么需要 Babel?

  • 浏览器兼容性:不同浏览器对新特性支持不一致
  • 开发效率:使用最新语法提高开发体验
  • 未来兼容:提前使用尚未广泛支持的提案特性
  • 生态统一:配合 TypeScript、React 等工具链

1.3 Babel 核心工作流程

  1. 解析(Parsing):将代码转换为抽象语法树(AST)
  2. 转换(Transforming):对 AST 进行各种操作
  3. 生成(Generating):将转换后的 AST 生成新代码

二、Babel 基础配置

2.1 基础安装

npm install --save-dev @babel/core @babel/cli @babel/preset-env

@babel/preset-env 不包含任何未进入 Stage 3 阶段的 JavaScript 语法提案,因为在 TC39 的流程中,未进入 Stage 3 阶段的提案是不会被任何浏览器所实现的。 如果确有需要,可以手动设置。通过设置 shippedProposals 参数可以包含进入 Stage 3 阶段并且已经被部分浏览器实现的提案

2.2 基础配置文件 babel.config.json

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "edge": "17",
          "firefox": "60",
          "chrome": "67",
          "safari": "11.1"
        },
        "useBuiltIns": "usage",
        "corejs": "3.6.5"
      }
    ]
  ]
}

2.3 运行 Babel

npx babel src --out-dir dist
  • src: 源文件夹
  • dist: 输出文件夹

2.4 常用 presets

Preset 功能
@babel/preset-env 智能转换 ES6+ 语法
@babel/preset-react 转换 JSX 语法
@babel/preset-typescript 转换 TypeScript

三、常用预设@babel/preset-env

3.1 基本配置

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "usage",
        "corejs": "3.32"
      }
    ]
  ]
}

四、自定义转换规则

4.1 插件 vs Preset(预设)

  • Preset:插件集合(如 @babel/preset-env 包含几十个插件)
  • 插件:单一功能转换(如 @babel/plugin-transform-arrow-functions

插件和预设可以组合使用也可以各自单独使用

这意味着如果两个转换插件都将处理“程序(Program)”的某个代码片段,则将根据转换插件或 preset 的排列顺序依次执行。

  • 插件在 Presets 前运行。
  • 插件顺序从前往后排列。
  • Preset 顺序是颠倒的(从后往前)

4.2 单一插件使用示例

箭头函数插件,此插件包含于@babel/preset-env中。

4.2.1 安装插件

npm install --save-dev @babel/plugin-transform-arrow-functions

4.2.2 在babel.config.json中添加配置

{
  "plugins": [["@babel/plugin-transform-arrow-functions", { "spec": true }]]
}
  • spec:这个是插件编译的选项,每个插件都有自己不同的编译选项

4.2.3 具体示例

// 源文件箭头函数
[1, 2, 3].map(n => n * 2);
// { "spec": false }]
"use strict";

[1, 2, 3].map(function (n) {
  return n * 2;
});
// { "spec": true }]
"use strict";

var _this = void 0;
function _newArrowCheck(n, r) { if (n !== r) throw new TypeError("Cannot instantiate an arrow function"); }
[1, 2, 3].map(function (n) {
  _newArrowCheck(this, _this);
  return n * 2;
}.bind(void 0));

结语

如果你喜欢本教程,记得点赞+收藏!关注我获取更多JavaScript开发干货。

前端常说的 SCSS是个啥玩意?一篇文章给你讲的明明白白!

作者 知否技术
2025年7月6日 21:49

0.邂逅 SCSS

我们在使用脚手架 Vue cli 或者 Vite 创建 Vue 项目的时候,会发现在 css 样式标签那里有一个 lang="scss" 的属性,那 scss 是什么玩意?

专业术语:SCSS 是一种预处理器语言,是 CSS 的一种扩展。

大白话: SCSS 就像是游戏中的 Buff,因为它支持定义变量、嵌套、混合等功能,所以它会让 CSS 变得更加强大,并且更容易维护。

使用 SCSS 这玩意之后,编译后的代码还是 CSS。

1. SCSS 核心知识点

1.1 变量

作用: 主要用来存一些重复使用的值,比如尺寸、颜色等等。方便统一管理。

定义变量语法规则: 以 **开头,并且** 开头,并且 后面不能直接跟数字。

// 定义变量
$bgColor: #a173f3;
$spaceSize: 20px;

// 使用变量
.header {
  background: $bgColor;
  padding: $spaceSize;
}

1.2 嵌套

作用:可以很直观的展示 html 结构层级关系,减少重复代码

.div {
  ul {
    li {
      display: block;
      a {
        color: red;
        &:hover {  // & 表示父选择器
          text-decoration: underline;
        }
      }
    }
  }
}

**注:**在 SCSS 中,& 符号用于引用父元素类名,实现嵌套规则的简化书写。例如:

.father {
  &-container {
    margin: 66px;
  }
  &-text {
    font-size: 20px;
  } 
}

编译后对应的CSS代码为:

.father-container {
   margin: 66px;
}
.father-text {
  font-size: 20px;
}

1.3 混合 (Mixins)

作用:就像 js 中的函数一样,是一个可以重复利用的样式代码块,并且支持参数。

@mixin 用来定义样式代码块,@include 用来使用样式代码块。

// 定义 Mixin
@mixin border-radius($radius) {
  border-radius: $radius;
  -webkit-border-radius: $radius;
  -moz-border-radius: $radius;
}

// 使用 Mixin
.button {
  @include border-radius(5px);
}

1.4 继承

作用:继承可以让一个选择器继承另一个选择器的样式。就是儿子继承爹的样式。

// 定义基础样式
.father-style {
  color: $primary-color;
  font-size: $font-size;
}

// 继承样式
.son-style {
  @extend .father-style;
  font-size: 20px;
}

1.5 运算

作用:在 css 中对数值进行加减乘除操作

$base-size: 10px;

.header-content {
  font-size: $base-size * 3;  // 30px
  width: 100% - 40%;             // 60%
  padding: ($base-size / 5);     // 2px
}

1.6 函数

作用:SCSS 提供了一些内置函数,也可以自定义函数,用来计算值。

// 使用内置函数
body {
  font-size: em(16px);
}

// 自定义函数
@function doubleTwo($num) {
  @return $num * 2;
}

// 使用自定义函数
.header {
  height: doubleTwo(66px);
}

1.7 条件与循环

作用:动态生成样式:if判断、for循环、each 遍历。

// If 判断
@mixin myStyle($flag: false) {
  @if $flag {
    background: red;
    color: white;
  } @else {
    background: green;
     color: black;
  }
}

// For 循环
@for $i from 1 to 10 {
  .col-#{$i} { 
    width: 100% / $i; 
  }
}

// Each 遍历
$sizes: small, medium, large;
@each $size in $sizes {
  .icon-#{$size} {
    font-size: if($size==large, 14px,20px);
  }
}

1.8 模块化

作用:就是根据一些功能拆分文件,便于统一管理和维护。 例如你可以新建一个 myVariables.scss 文件,专门用来存储变量:

// 定义变量
$primary-color: #3FAF60;
$background-color: red;
$font-size: 18px;

然后在另外一个 scss 文件中导入使用:

@import "../scss/myVariables.css";
.button {
  padding: 15px;
  font-size: $font-size;
  background-color: $primary-color;
}

其实我们常用的做法是建一个 main.scss 文件,然后导入一些全局样式:

// 导入基础样式
@import "base/variables";
@import "base/mixins";

// 导入组件样式
@import "components/button";
@import "components/card";

// 导入布局样式
@import "layout/header";
@import "layout/footer";
@import "layout/sidebar";

// 导入页面样式
@import "pages/home";
@import "pages/about";

// 导入主题样式
@import "themes/light";

2. 在 Vue 中使用 SCSS

其实我们使用的脚手架 Vue cli 或者 Vite 本身都集成了对 scss 语法的支持,只不过我们需要在项目中安装依赖:

# Vue CLI 脚手架
npm install sass sass-loader@10 -D

# Vite 脚手架
npm install sass -D

安装依赖之后我们在 style 标签上面添加 lang="scss" 表示对 scss 的支持。另外我们通常会加上 scoped 属性表示这些样式只限制在本页面使用。

3. 答疑

3.1 sass 和 scss 什么关系?

我们在安装 scss 依赖时发现是 npm install sass。那 sass 和 scss 什么关系?

回答:

sass 和 scss 是同一种预处理器语言的不同语法版本,scss 是 Sass 的超集,兼容 sass 语法和 CSS 语法。

其实 CSS 最开始的预处理语言是 sass,后来 sass 3 版本引入了 scss 的语法扩展。说白了就是 scss 比 sass 更牛逼,更好用,但是 scss 基于 sass。所以你 npm install 的是 sass。

3.2 scss 和 css 到底什么关系

**回答:**SCSS 可以看作是增强版的 CSS,支持变量、嵌套、混合等一些非常高级的特性。

🎨前端实现文字渐变的三种方式

2025年7月6日 20:29

🎨 前言

最近开发的时候发现很多ui图上面的标题都是带有渐变效果的,这里就记录一下前端实现文字渐变的几种方式。

完整效果如下

image-20250706195210431

🎯 CSS 方式

通过给文字容器的背景设置渐变颜色,并使用background-clip属性,将其以文字内容进行裁切。最后使用text-fill-color属性,给文字设置透明填充来实现

属性名称 效果
background linear-gradient(to top, #b1495a, #c71a44) 给文字容器设置渐变背景色
background-clip text 背景被裁切成文字的前景色
text-fill-color transparent 文字的填充颜色

效果如下

image-20250706193610605

  • 具体样式代码
.up-gradient {
  background: linear-gradient(to top, #b1495a, #c71a44);
  /* 背景被裁剪成文字的前景色。 */
  background-clip: text;
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  /* 文字透明填充 */
  text-fill-color: transparent;
}
.down-gradient {
  background: linear-gradient(to bottom, #b1495a, #c71a44);
  background-clip: text;
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  /* 文字透明填充 */
  text-fill-color: transparent;
}
.left-gradient {
  background: linear-gradient(to left, #b1495a, #c71a44);
  background-clip: text;
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  /* 文字透明填充 */
  text-fill-color: transparent;
}
.right-gradient {
  background: linear-gradient(to right, #b1495a, #c71a44);
  background-clip: text;
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  /* 文字透明填充 */
  text-fill-color: transparent;
}
/* 多颜色渐变 */
.multi-gradient {
  background: linear-gradient(90deg, #b1495a 10%, #c71a44 50%, #ffb86c 80%);
  background-clip: text;
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  /* 文字透明填充 */
  text-fill-color: transparent;
}
  • html 结构

    <body>
      <div class="container">
        <h1>CSS实现文字渐变</h1>
        <!-- css版本 -->
        <article class="panel">
          <div class="panel-box-title">CSS版:</div>
          <div class="box">
            <div class="content-text up-gradient">向上渐变</div>
            <div class="content-text down-gradient">向下渐变</div>
            <div class="content-text left-gradient">向左渐变</div>
            <div class="content-text right-gradient">向右渐变</div>
            <!-- 设置多个颜色 -->
            <div class="content-text multi-gradient">多颜色渐变</div>
          </div>
        </article>
      </div>
    </body>
    

🎨 Canvas 方式

canvas中的文字渐变的实现方式就很简单了,因为canvas可以直接给文字设置渐变样式。

主要用到createLinearGradient方法,用来创建一个线性渐变,addColorStop设置渐变的色标,就像是这个效果

image-20250706194243790

最后再用fillStyle指定使用我们创建的渐变对象即可

效果如下

image-20250706194357233

核心代码

<!DOCTYPE html>
<html lang="zh-cn">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>文字渐变</title>
    <link rel="stylesheet" href="index.css" />
  </head>
  <body>
    <div class="container">
      <!-- canvas版本 -->
      <article class="panel">
        <div class="panel-box-title">Canvas版:</div>
        <div class="box">
          <canvas id="canvas" height="180" width="900"></canvas>
        </div>
      </article>
    </div>
  </body>
  <script>
    const canvas = document.getElementById('canvas')
    const ctx = canvas.getContext('2d')
    ctx.font = '32px Arial'
    // 从左到右的渐变文字
    const leftToRightGradient = ctx.createLinearGradient(0, 0, canvas.width, 0)
    leftToRightGradient.addColorStop(0, '#fff')
    leftToRightGradient.addColorStop(1, '#000')

    ctx.fillStyle = leftToRightGradient
    ctx.fillText('Canvas 从左到右渐变', 20, 40)

    // 从上到下的渐变文字
    const topToBottomGradient = ctx.createLinearGradient(0, 0, 0, canvas.height)
    topToBottomGradient.addColorStop(0, '#fff')
    topToBottomGradient.addColorStop(1, '#000')

    ctx.fillStyle = topToBottomGradient
    ctx.fillText('Canvas 从上到下渐变', 20, 80)

    // 从右到左的渐变文字
    const rightToLeftGradient = ctx.createLinearGradient(canvas.width, 0, 0, 0)
    rightToLeftGradient.addColorStop(0, '#fff')
    rightToLeftGradient.addColorStop(1, '#000')

    ctx.fillStyle = rightToLeftGradient
    ctx.fillText('Canvas 从右到左渐变', 20, 120)

    // 从下到上的渐变文字
    const bottomToTopGradient = ctx.createLinearGradient(0, canvas.height, 0, 0)
    bottomToTopGradient.addColorStop(0, '#fff')
    bottomToTopGradient.addColorStop(1, '#000')

    ctx.fillStyle = bottomToTopGradient
    ctx.fillText('Canvas 从下到上渐变', 20, 160)
  </script>
</html>

🎭 SVG 方式

SVG 文字渐变的核心原理是使用 SVG 的<linearGradient>定义渐变,然后通过fill="url(#gradientId)"将渐变应用到文字上。

渐变效果如下

image-20250706195014066

核心代码如下

    <svg width="900" height="180" xmlns="http://www.w3.org/2000/svg">
            <defs>
              <!-- 从左到右渐变 -->
              <linearGradient
                id="leftToRight"
                x1="0%"
                y1="0%"
                x2="100%"
                y2="0%"
              >
                <stop
                  offset="0%"
                  style="stop-color: #b1495a; stop-opacity: 1"
                />
                <stop
                  offset="100%"
                  style="stop-color: #c71a44; stop-opacity: 1"
                />
              </linearGradient>

              <!-- 从上到下渐变 -->
              <linearGradient
                id="topToBottom"
                x1="0%"
                y1="0%"
                x2="0%"
                y2="100%"
              >
                <stop
                  offset="0%"
                  style="stop-color: #b1495a; stop-opacity: 1"
                />
                <stop
                  offset="100%"
                  style="stop-color: #c71a44; stop-opacity: 1"
                />
              </linearGradient>

              <!-- 从右到左渐变 -->
              <linearGradient
                id="rightToLeft"
                x1="100%"
                y1="0%"
                x2="0%"
                y2="0%"
              >
                <stop
                  offset="0%"
                  style="stop-color: #b1495a; stop-opacity: 1"
                />
                <stop
                  offset="100%"
                  style="stop-color: #c71a44; stop-opacity: 1"
                />
              </linearGradient>

              <!-- 从下到上渐变 -->
              <linearGradient
                id="bottomToTop"
                x1="0%"
                y1="100%"
                x2="0%"
                y2="0%"
              >
                <stop
                  offset="0%"
                  style="stop-color: #b1495a; stop-opacity: 1"
                />
                <stop
                  offset="100%"
                  style="stop-color: #c71a44; stop-opacity: 1"
                />
              </linearGradient>
            </defs>

            <!-- 从左到右渐变文字 -->
            <text
              x="20"
              y="40"
              font-family="Arial"
              font-size="32"
              font-weight="bold"
              fill="url(#leftToRight)"
            >
              SVG 从左到右渐变
            </text>

            <!-- 从上到下渐变文字 -->
            <text
              x="20"
              y="80"
              font-family="Arial"
              font-size="32"
              font-weight="bold"
              fill="url(#topToBottom)"
            >
              SVG 从上到下渐变
            </text>

            <!-- 从右到左渐变文字 -->
            <text
              x="20"
              y="120"
              font-family="Arial"
              font-size="32"
              font-weight="bold"
              fill="url(#rightToLeft)"
            >
              SVG 从右到左渐变
            </text>

            <!-- 从下到上渐变文字 -->
            <text
              x="20"
              y="160"
              font-family="Arial"
              font-size="32"
              font-weight="bold"
              fill="url(#bottomToTop)"
            >
              SVG 从下到上渐变
            </text>
          </svg>

📝 完整示例代码

index.css

样式代码

html,
body {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
body {
  background-color: #000;
  color: #fff;
  font-family: 'Segoe UI', 'Arial', sans-serif;
  font-size: 16px;
  line-height: 1.5;
  display: flex;
  flex-direction: column;
  align-items: center;
  user-select: none;
}

/* 外层容器 */
.container {
  width: 80%;
  max-width: 900px;
  margin: 40px auto;
  padding: 24px;
  background: #181c24;
  border-radius: 18px;
  box-shadow: 0 8px 40px rgba(0, 0, 0, 0.45);
  border: 1.5px solid #232936;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
}
.panel {
  position: relative;
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.panel-box-title {
  font-weight: bold;
  color: #ffb86c;
  text-shadow: 0 2px 8px #181c24cc;
}
/* 通用文字样式 */
.content-text {
  font-size: 32px;
  font-weight: bold;
}

.box {
  background: #191b22;
  border-radius: 14px;
  padding: 24px;
  box-shadow: 0 4px 32px rgba(0, 0, 0, 0.32);
  display: flex;
  flex-direction: column;
  border: 1.5px solid #232936;
  transition: box-shadow 0.2s, border 0.2s;
  position: relative;
  overflow: hidden;
  z-index: 1;
}
.box:hover {
  box-shadow: 0 8px 48px 0 rgba(0, 0, 0, 0.76);
  border: 1.5px solid #3a3f4b;
}

.up-gradient {
  background: linear-gradient(to top, #b1495a, #c71a44);
  /* 背景被裁剪成文字的前景色。 */
  background-clip: text;
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  /* 文字透明填充 */
  text-fill-color: transparent;
}
.down-gradient {
  background: linear-gradient(to bottom, #b1495a, #c71a44);
  background-clip: text;
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  /* 文字透明填充 */
  text-fill-color: transparent;
}
.left-gradient {
  background: linear-gradient(to left, #b1495a, #c71a44);
  background-clip: text;
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  /* 文字透明填充 */
  text-fill-color: transparent;
}
.right-gradient {
  background: linear-gradient(to right, #b1495a, #c71a44);
  background-clip: text;
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  /* 文字透明填充 */
  text-fill-color: transparent;
}
/* 多颜色渐变 */
.multi-gradient {
  background: linear-gradient(90deg, #b1495a 10%, #c71a44 50%, #ffb86c 80%);
  background-clip: text;
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  /* 文字透明填充 */
  text-fill-color: transparent;
}

index.html

<!DOCTYPE html>
<html lang="zh-cn">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>文字渐变</title>
    <link rel="stylesheet" href="index.css" />
  </head>
  <body>
    <div class="container">
      <h1>前端实现文字渐变的几种方式</h1>
      <!-- css版本 -->
      <article class="panel">
        <div class="panel-box-title">CSS版:</div>
        <div class="box">
          <div class="content-text up-gradient">向上渐变</div>
          <div class="content-text down-gradient">向下渐变</div>
          <div class="content-text left-gradient">向左渐变</div>
          <div class="content-text right-gradient">向右渐变</div>
          <!-- 设置多个颜色 -->
          <div class="content-text multi-gradient">多颜色渐变</div>
        </div>
      </article>
      <!-- canvas版本 -->
      <article class="panel">
        <div class="panel-box-title">Canvas版:</div>
        <div class="box">
          <canvas id="canvas" height="180" width="900"></canvas>
        </div>
      </article>
      <!-- svg版本 -->
      <article class="panel">
        <div class="panel-box-title">SVG版:</div>
        <div class="box">
          <svg width="900" height="180" xmlns="http://www.w3.org/2000/svg">
            <defs>
              <!-- 从左到右渐变 -->
              <linearGradient
                id="leftToRight"
                x1="0%"
                y1="0%"
                x2="100%"
                y2="0%"
              >
                <stop
                  offset="0%"
                  style="stop-color: #b1495a; stop-opacity: 1"
                />
                <stop
                  offset="100%"
                  style="stop-color: #c71a44; stop-opacity: 1"
                />
              </linearGradient>

              <!-- 从上到下渐变 -->
              <linearGradient
                id="topToBottom"
                x1="0%"
                y1="0%"
                x2="0%"
                y2="100%"
              >
                <stop
                  offset="0%"
                  style="stop-color: #b1495a; stop-opacity: 1"
                />
                <stop
                  offset="100%"
                  style="stop-color: #c71a44; stop-opacity: 1"
                />
              </linearGradient>

              <!-- 从右到左渐变 -->
              <linearGradient
                id="rightToLeft"
                x1="100%"
                y1="0%"
                x2="0%"
                y2="0%"
              >
                <stop
                  offset="0%"
                  style="stop-color: #b1495a; stop-opacity: 1"
                />
                <stop
                  offset="100%"
                  style="stop-color: #c71a44; stop-opacity: 1"
                />
              </linearGradient>

              <!-- 从下到上渐变 -->
              <linearGradient
                id="bottomToTop"
                x1="0%"
                y1="100%"
                x2="0%"
                y2="0%"
              >
                <stop
                  offset="0%"
                  style="stop-color: #b1495a; stop-opacity: 1"
                />
                <stop
                  offset="100%"
                  style="stop-color: #c71a44; stop-opacity: 1"
                />
              </linearGradient>
            </defs>

            <!-- 从左到右渐变文字 -->
            <text
              x="20"
              y="40"
              font-family="Arial"
              font-size="32"
              font-weight="bold"
              fill="url(#leftToRight)"
            >
              SVG 从左到右渐变
            </text>

            <!-- 从上到下渐变文字 -->
            <text
              x="20"
              y="80"
              font-family="Arial"
              font-size="32"
              font-weight="bold"
              fill="url(#topToBottom)"
            >
              SVG 从上到下渐变
            </text>

            <!-- 从右到左渐变文字 -->
            <text
              x="20"
              y="120"
              font-family="Arial"
              font-size="32"
              font-weight="bold"
              fill="url(#rightToLeft)"
            >
              SVG 从右到左渐变
            </text>

            <!-- 从下到上渐变文字 -->
            <text
              x="20"
              y="160"
              font-family="Arial"
              font-size="32"
              font-weight="bold"
              fill="url(#bottomToTop)"
            >
              SVG 从下到上渐变
            </text>
          </svg>
        </div>
      </article>
    </div>
  </body>
  <script>
    const canvas = document.getElementById('canvas')
    const ctx = canvas.getContext('2d')
    ctx.font = '32px Arial'
    // 从左到右的渐变文字
    const leftToRightGradient = ctx.createLinearGradient(0, 0, canvas.width, 0)
    leftToRightGradient.addColorStop(0, '#fff')
    leftToRightGradient.addColorStop(1, '#000')

    ctx.fillStyle = leftToRightGradient
    ctx.fillText('Canvas 从左到右渐变', 20, 40)

    // 从上到下的渐变文字
    const topToBottomGradient = ctx.createLinearGradient(0, 0, 0, canvas.height)
    topToBottomGradient.addColorStop(0, '#fff')
    topToBottomGradient.addColorStop(1, '#000')

    ctx.fillStyle = topToBottomGradient
    ctx.fillText('Canvas 从上到下渐变', 20, 80)

    // 从右到左的渐变文字
    const rightToLeftGradient = ctx.createLinearGradient(canvas.width, 0, 0, 0)
    rightToLeftGradient.addColorStop(0, '#fff')
    rightToLeftGradient.addColorStop(1, '#000')

    ctx.fillStyle = rightToLeftGradient
    ctx.fillText('Canvas 从右到左渐变', 20, 120)

    // 从下到上的渐变文字
    const bottomToTopGradient = ctx.createLinearGradient(0, canvas.height, 0, 0)
    bottomToTopGradient.addColorStop(0, '#fff')
    bottomToTopGradient.addColorStop(1, '#000')

    ctx.fillStyle = bottomToTopGradient
    ctx.fillText('Canvas 从下到上渐变', 20, 160)
  </script>
</html>

🎉 结尾

日常开发中还是css版本的比较常用。另外两种,只有在特定环境下才有用。

前端面试第 75 期 - 2025.07.06 更新前端面试问题总结(12道题)

作者 晴小篆
2025年7月6日 20:06

2025.03.25 - 2025.07.06 更新前端面试问题总结(12道题)
获取更多面试相关问题可以访问
github 地址: github.com/pro-collect…
gitee 地址: gitee.com/yanleweb/in…

目录

中级开发者相关问题【共计 5 道题】

  1. 介绍一下 Web Components和Shadow DOM【热度: 489】【工程化】
  2. 前端倒计时有误差怎么解决【热度: 454】【web应用场景】【出题公司: 阿里巴巴】
  3. 让元素滚动到可视区, 有那些办法【热度: 320】【web应用场景】【出题公司: 阿里巴巴】
  4. scrollIntoView、scrollIntoViewIfNeeded、scrollTo 有何区别?【热度: 320】【web应用场景】【出题公司: 阿里巴巴】
  5. decodeURI 和 decodeURIComponent 有何区别【热度: 230】【web应用场景】【出题公司: 京东】

高级开发者相关问题【共计 5 道题】

  1. 解释 V8 引擎的内存管理机制,如何通过优化闭包或对象结构减少内存泄漏?【热度: 748】【工程化】
  2. Web Components 有哪些优势【热度: 489】【工程化】
  3. 为何 Web Components 没有成为 web 前端的主流技术框架, 反而是 react 和 vue 呢?【热度: 531】【工程化】
  4. 很多web前端框架里面会有约定式路由, 他们是如何实现的【热度: 331】【工程化】【出题公司: 阿里巴巴】
  5. 如何对一个大型 SPA(单页应用)进行全面的性能优化?请从构建阶段、运行时、网络请求、渲染等多个维度说明。【热度: 761】【工程化】【出题公司: 腾讯】

资深开发者相关问题【共计 2 道题】

  1. 微前端架构有哪些主流框架可以选, 各有啥优劣势?【热度: 554】【工程化】【出题公司: 腾讯】

  2. 如何将一个非常到大的 spa 应用, 迁移到微前端架构, 有哪些考虑因素【热度: 754】【工程化】

中级开发者相关问题【共计 5 道题】

1111. 介绍一下 Web Components和Shadow DOM【热度: 489】【工程化】

关键词:Web Components

Web Components 和 Shadow DOM 详解

一、Web Components 概述

Web Components 是一套用于构建可复用、封装性强的 Web 组件的标准,它允许开发者创建自定义 HTML 元素,这些元素具有独立的功能、样式和行为,可在不同项目中重复使用。

二、Web Components 的核心组成部分
  • Custom Elements:定义自定义 HTML 元素的 API,通过继承 HTMLElement 或其他内置元素类来创建新元素。
    • 示例
      class MyButton extends HTMLElement {
        connectedCallback() {
          this.innerHTML = "<button>点击我</button>";
        }
      }
      customElements.define("my-button", MyButton);
      
  • HTML Templates:使用 <template> 标签定义可复用的模板,模板内容在运行时才会被解析。
    • 示例
      <template id="buttonTemplate">
        <style>
          button {
            color: blue;
          }
        </style>
        <button>自定义按钮</button>
      </template>
      
  • Shadow DOM:为组件创建独立的 DOM 树和样式作用域,避免与外部样式冲突。
  • HTML Imports(已被 ES 模块取代):导入外部 HTML 文件以复用组件结构。
三、Shadow DOM 详解

Shadow DOM 是 Web Components 的关键特性,它为组件提供了封装的 DOM 环境,具有以下核心特点:

(一)Shadow DOM 的核心概念
  • Shadow Root:Shadow DOM 的根节点,通过 element.attachShadow() 方法创建。
  • Light DOM:宿主元素的原始 DOM 内容。
  • Shadow DOM 与 Light DOM 的融合:通过 <slot> 元素将 Light DOM 内容插入到 Shadow DOM 中。
(二)创建 Shadow DOM 的步骤
  1. 创建 Shadow Root
    const shadowRoot = element.attachShadow({ mode: "open" }); // open 模式允许外部访问 shadowRoot
    // 或 mode: 'closed' 模式禁止外部访问
    
  2. 向 Shadow Root 中添加内容
    shadowRoot.innerHTML = `
      <style>p { color: red; }</style>
      <p>Shadow DOM 内容</p>
    `;
    
(三)Shadow DOM 的作用
  • 样式隔离:Shadow DOM 内的样式不会影响外部,外部样式也不会渗透到内部(除非使用特殊选择器)。
  • 结构封装:组件内部 DOM 结构对外部不可见,避免被意外修改。
  • Slot 分发机制:通过 <slot name="xxx"> 定义插槽,允许外部内容以灵活方式插入组件。
四、Web Components 与 Shadow DOM 的关系
  • Shadow DOM 是 Web Components 的实现基础:通过 Shadow DOM 实现组件的样式和 DOM 封装。
  • 结合使用场景
    1. 创建自定义按钮、表单控件等可复用组件。
    2. 构建复杂页面模块(如导航栏、卡片组件),避免样式冲突。
    3. 封装第三方组件,防止其样式污染页面。
五、浏览器兼容性与 Polyfill
  • 兼容性:现代浏览器(Chrome、Firefox、Safari 等)已广泛支持,但 IE 及旧版 Edge 不支持。
  • Polyfill 库:如 lit-elementpolymer 可用于兼容旧浏览器。
六、实际应用案例
  • 原生组件<video><audio> 等标签内部使用了 Shadow DOM。
  • 框架实践:Vue 3 的单文件组件(.vue)、React 的 CSS-in-JS 方案借鉴了组件封装思想。
  • 开源组件库:如 Material Components for the Web 基于 Web Components 构建。
七、总结

Web Components 和 Shadow DOM 是前端组件化的重要标准,通过封装性解决了传统前端开发中样式污染、代码复用性差的问题,为构建大型应用提供了更规范的解决方案。随着浏览器支持度的提升,它们已成为现代前端开发的核心技术之一。

1114. 前端倒计时有误差怎么解决【热度: 454】【web应用场景】【出题公司: 阿里巴巴】

前端倒计时出现误差是常见问题,主要由 JavaScript 的单线程特性、浏览器优化(如标签页切换时的节流)以及事件循环调度延迟引起。以下是几种解决方案:

1. 使用高精度时间戳(performance.now())

Date.now() 依赖系统时间,可能被用户或系统修改;而 performance.now() 提供更精确的时间测量,且不受系统时间影响。

function countDown(targetTime) {
  const startTime = performance.now();
  const totalMs = targetTime - Date.now(); // 目标时间与当前时间的差值

  function update() {
    const elapsedMs = performance.now() - startTime;
    const remainingMs = Math.max(0, totalMs - elapsedMs);

    // 更新UI
    const seconds = Math.floor(remainingMs / 1000);
    console.log(`剩余时间:${seconds}秒`);

    if (remainingMs > 0) {
      requestAnimationFrame(update);
    }
  }

  requestAnimationFrame(update);
}

2. 定期同步服务器时间

通过 AJAX 请求定期获取服务器时间,减少累计误差:

let serverTimeOffset = 0;

// 同步服务器时间
async function syncServerTime() {
  try {
    const response = await fetch("/api/time"); // 后端接口返回当前时间戳
    const serverTime = await response.json();
    serverTimeOffset = serverTime - Date.now();
  } catch (error) {
    console.error("同步服务器时间失败:", error);
  }
}

// 初始化同步
syncServerTime();
// 每小时同步一次
setInterval(syncServerTime, 3600000);

// 使用同步后的时间计算倒计时
function getAccurateTime() {
  return Date.now() + serverTimeOffset;
}

3. 动态调整间隔

根据实际流逝时间与预期流逝时间的差值,动态调整下一次执行的延迟:

function preciseInterval(callback, delay) {
  let nextTime = Date.now() + delay;

  function interval() {
    const currentTime = Date.now();
    const drift = currentTime - nextTime; // 计算误差

    callback();
    nextTime += delay;

    // 动态调整下一次执行时间
    const nextDelay = Math.max(0, delay - drift);
    setTimeout(interval, nextDelay);
  }

  setTimeout(interval, delay);
}

// 使用示例
preciseInterval(() => {
  console.log("精确执行");
}, 1000);

4. 后台倒计时(Web Worker)

将倒计时逻辑放在 Web Worker 中,避免主线程阻塞:

// main.js
const worker = new Worker("worker.js");

worker.onmessage = (e) => {
  if (e.data.type === "update") {
    console.log(`剩余时间:${e.data.seconds}秒`);
  }
};

// worker.js
let targetTime;

self.onmessage = (e) => {
  if (e.data.type === "start") {
    targetTime = e.data.targetTime;
    startCountdown();
  }
};

function startCountdown() {
  function update() {
    const remainingMs = Math.max(0, targetTime - Date.now());
    const seconds = Math.floor(remainingMs / 1000);

    self.postMessage({ type: "update", seconds });

    if (remainingMs > 0) {
      setTimeout(update, 1000);
    }
  }

  update();
}

5. 结合 requestAnimationFrame

利用 requestAnimationFrame 的高刷新率(约 60fps)实现平滑倒计时:

function smoothCountdown(targetTime) {
  function update() {
    const remainingMs = Math.max(0, targetTime - Date.now());
    const seconds = Math.floor(remainingMs / 1000);

    // 更新UI
    console.log(`剩余时间:${seconds}秒`);

    if (remainingMs > 0) {
      requestAnimationFrame(update);
    }
  }

  requestAnimationFrame(update);
}

最佳实践总结

  1. 优先使用高精度时间performance.now()Date.now() 更可靠。

  2. 定期同步服务器时间:减少长时间运行的累计误差。

  3. 动态调整间隔:补偿 JavaScript 事件循环的延迟。

  4. 避免长时间 setTimeout:改用递归 setTimeoutrequestAnimationFrame

  5. 标签页可见性处理:使用 document.visibilityState 在页面不可见时暂停倒计时,可见时重新同步。

1115. 让元素滚动到可视区, 有那些办法【热度: 320】【web应用场景】【出题公司: 阿里巴巴】

在前端开发中,使元素滚动到可视区域是常见需求。以下是几种实现方式,从简单到复杂逐步介绍:

1. Element.scrollIntoView()

这是最简单的原生方法,支持平滑滚动和对齐方式:

// 立即滚动到元素顶部与视口顶部对齐
element.scrollIntoView();

// 平滑滚动到元素底部与视口底部对齐
element.scrollIntoView({
  behavior: "smooth", // 平滑滚动
  block: "end", // 垂直对齐方式:start | center | end | nearest
  inline: "nearest", // 水平对齐方式:start | center | end | nearest
});

优点:简单易用,兼容性好(IE11+)。
缺点:无法精确控制滚动速度或添加自定义动画。

2. Window.scrollTo() 或 window.scrollBy()

计算元素位置后滚动窗口:

// 获取元素相对于文档顶部的位置
const rect = element.getBoundingClientRect();
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const targetY = rect.top + scrollTop;

// 平滑滚动到目标位置
window.scrollTo({
  top: targetY,
  behavior: "smooth",
});

// 或者使用 scrollBy 相对滚动
window.scrollBy({
  top: rect.top, // 相对于当前位置的偏移量
  behavior: "smooth",
});

优点:灵活控制目标位置。
缺点:需手动计算位置,不适合复杂布局。

3. 自定义平滑滚动动画

使用 requestAnimationFrame 实现更精细的滚动控制:

function smoothScroll(element) {
  const target = element.getBoundingClientRect().top;
  const duration = 500; // 动画持续时间(毫秒)
  let startTime = null;

  function animation(currentTime) {
    if (!startTime) startTime = currentTime;
    const timeElapsed = currentTime - startTime;
    const progress = Math.min(timeElapsed / duration, 1);
    const easeProgress = progress === 1 ? 1 : 1 - Math.pow(2, -10 * progress); // 缓动函数

    window.scrollTo(0, window.scrollY + target * easeProgress);

    if (progress < 1) {
      requestAnimationFrame(animation);
    }
  }

  requestAnimationFrame(animation);
}

// 使用示例
smoothScroll(document.getElementById("target"));

优点:完全自定义动画效果和速度曲线。
缺点:代码复杂度较高。

4. 滚动容器内元素定位

如果元素在滚动容器内(而非整个页面),需滚动容器本身:

const container = document.getElementById("scroll-container");
const child = document.getElementById("child-element");

// 计算子元素相对于容器的位置
const containerRect = container.getBoundingClientRect();
const childRect = child.getBoundingClientRect();
const offsetTop = childRect.top - containerRect.top;

// 滚动容器
container.scrollTo({
  top: container.scrollTop + offsetTop,
  behavior: "smooth",
});

5. CSS Scroll Snap

使用 CSS scroll-snap-type 创建吸附效果:

.scroll-container {
  scroll-snap-type: y mandatory; /* 垂直滚动,强制吸附 */
  overflow-y: auto;
  height: 300px; /* 容器高度 */
}

.scroll-item {
  scroll-snap-align: start; /* 吸附到容器起始位置 */
  height: 100%; /* 每个项目占满容器高度 */
}
<div class="scroll-container">
  <div class="scroll-item">项目1</div>
  <div class="scroll-item">项目2</div>
  <div class="scroll-item">项目3</div>
</div>

优点:纯 CSS 实现,性能优秀。
缺点:仅控制吸附位置,无法主动触发滚动。

6. 使用第三方库

smooth-scrollscrollreveal

// 安装:npm install smooth-scroll
import SmoothScroll from "smooth-scroll";

// 初始化
const scroll = new SmoothScroll('a[href*="#"]', {
  speed: 500,
  easing: "easeInOutCubic",
});

// 触发滚动
scroll.animateScroll(document.getElementById("target"));

选择建议

  • 简单场景:优先使用 scrollIntoView()
  • 需要自定义动画:使用 requestAnimationFrame 或第三方库。
  • 容器内滚动:操作容器的 scrollTop/scrollLeft
  • 固定吸附点:使用 CSS scroll-snap-type

无论选择哪种方式,都要考虑元素是否在视口中、滚动方向以及用户设备兼容性。

1116. scrollIntoView、scrollIntoViewIfNeeded、scrollTo 有何区别?【热度: 320】【web应用场景】【出题公司: 阿里巴巴】

scrollIntoView()scrollIntoViewIfNeeded()scrollTo() 是 JavaScript 中用于滚动的三个方法,它们的功能和适用场景有所不同:

1. Element.scrollIntoView()

  • 所属对象:DOM 元素(Element)。
  • 作用:将调用该方法的元素滚动到浏览器窗口的可视区域内。
  • 参数
    • behavior:滚动行为,可选 smooth(平滑滚动)或 auto(瞬间滚动,默认值)。
    • block:垂直对齐方式,可选 start(元素顶部与视口顶部对齐)、center(居中)、end(底部对齐)或 nearest(最近边缘)。
    • inline:水平对齐方式,可选 startcenterendnearest
// 平滑滚动到元素顶部对齐
element.scrollIntoView({ behavior: "smooth", block: "start" });
  • 兼容性:所有现代浏览器 + IE11。

2. Element.scrollIntoViewIfNeeded()

  • 所属对象:DOM 元素(Element)。
  • 作用仅在元素当前不在可视区域内时,将其滚动到可视区域。如果元素已可见,则不执行滚动。
  • 参数
    • centerIfNeeded:布尔值(仅 WebKit 浏览器支持,如 Chrome/Safari)。
      • true:将元素居中显示(默认值)。
      • false:将元素滚动到最近的边缘(顶部或底部)。
// 仅在元素不可见时滚动(Chrome/Safari)
element.scrollIntoViewIfNeeded(true);
  • 兼容性:Chrome、Safari 完全支持,Firefox 部分支持,IE/Edge 不支持

3. Window.scrollTo() / Element.scrollTo()

  • 所属对象
    • window.scrollTo():滚动整个页面。
    • element.scrollTo():滚动特定容器(如 <div class="scrollable">)。
  • 作用:滚动到指定的坐标位置。
  • 参数
    • 坐标方式scrollTo(x, y),指定目标位置的绝对坐标。
    • 选项对象
      • top:垂直滚动位置(像素)。
      • left:水平滚动位置(像素)。
      • behavior:滚动行为,同 scrollIntoView()
// 滚动到页面 (0, 500) 位置
window.scrollTo({ top: 500, behavior: "smooth" });

// 滚动容器内的元素
const container = document.querySelector(".scrollable");
container.scrollTo({ left: 200, behavior: "smooth" });

核心区别总结

方法 作用对象 触发条件 定位方式 兼容性
scrollIntoView() 元素自身 始终触发滚动 基于元素位置对齐 全浏览器支持
scrollIntoViewIfNeeded() 元素自身 仅元素不可见时触发 自动选择最佳位置 Chrome/Safari
scrollTo() 窗口或容器 始终触发滚动 指定绝对坐标 全浏览器支持

使用场景建议

  • 将元素显示在视口中:用 scrollIntoView(),适合固定导航栏跳转或表单错误定位。
  • 避免不必要的滚动:用 scrollIntoViewIfNeeded(),适合懒加载内容或动态列表。
  • 精确控制滚动位置:用 scrollTo(),适合实现进度条或分步表单。

例如:

// 平滑滚动到页面顶部
window.scrollTo({ top: 0, behavior: "smooth" });

// 将错误提示滚动到可视区
errorElement.scrollIntoView({ block: "center", behavior: "smooth" });

// 仅在图片不可见时滚动到它(Chrome/Safari)
imageElement.scrollIntoViewIfNeeded();

选择合适的方法能提升用户体验,避免不必要的页面抖动。

1117. decodeURI 和 decodeURIComponent 有何区别【热度: 230】【web应用场景】【出题公司: 京东】

decodeURI()decodeURIComponent() 是 JavaScript 中用于解码 URI(统一资源标识符)的两个方法,它们的核心区别在于解码范围和适用场景。

1. 编码规则回顾

在 URI 中,某些字符(如空格、特殊符号)需要被编码为 % 后跟两位十六进制数。例如:

  • 空格被编码为 %20
  • & 被编码为 %26
  • # 被编码为 %23

2. 核心区别

方法 解码范围 保留字符 典型应用场景
decodeURI() 解码整个 URI(如 http://example.com/path?query=value ;/?:@&=+$,#(URI 分隔符) 解码完整 URL
decodeURIComponent() 解码 URI 的组件部分(如查询参数、路径片段) 无保留字符(解码所有 % 编码) 解码查询参数或路径参数

3. 示例对比

场景 1:解码完整 URL
const encodedUrl = "http://example.com/path%2Fsubpath?param1=value1%26param2=value2";

// 使用 decodeURI()
decodeURI(encodedUrl);
// 输出:http://example.com/path/subpath?param1=value1%26param2=value2
// 注意:路径分隔符 `/`(%2F)和解码,但查询参数中的 `&`(%26)未解码

// 使用 decodeURIComponent()
decodeURIComponent(encodedUrl);
// 报错:URIError: malformed URI sequence
// 原因:完整 URL 中的分隔符(如 `?`、`&`)被错误解码
场景 2:解码查询参数
const encodedParam = "key1=value1%26key2=value2%23hash";

// 使用 decodeURI()
decodeURI(encodedParam);
// 输出:key1=value1%26key2=value2%23hash
// 注意:`&`(%26)和 `#`(%23)未被解码

// 使用 decodeURIComponent()
decodeURIComponent(encodedParam);
// 输出:key1=value1&key2=value2#hash
// 正确解码所有参数部分

4. 常见误区

  • 误用 decodeURI() 处理参数:若 URL 参数包含 &= 等符号,decodeURI() 不会解码它们,导致参数解析错误。

    // 错误示例:查询参数中的 `&` 未被解码
    const query = "name=John%26Doe";
    decodeURI(query); // "name=John%26Doe"(错误)
    
    // 正确方式
    decodeURIComponent(query); // "name=John&Doe"(正确)
    
  • 误用 decodeURIComponent() 处理完整 URL:会破坏 URL 结构(如路径分隔符被解码)。

    const url = "http://example.com/path%3Fparam=value"; // 假设路径中包含 `?`
    decodeURIComponent(url); // "http://example.com/path?param=value"(错误,路径被截断)
    

5. 总结

  • 使用 decodeURI()

    • 处理完整 URL(如 window.location.href)。
    • 保留 URI 中的特殊分隔符(如 ?&/)。
  • 使用 decodeURIComponent()

    • 处理 URI 的组件部分(如查询参数、路径参数)。
    • 需要解码所有特殊字符(如表单提交的参数)。

口诀

  • 完整 URLdecodeURI()

  • 参数片段decodeURIComponent()

高级开发者相关问题【共计 5 道题】

1110. 解释 V8 引擎的内存管理机制,如何通过优化闭包或对象结构减少内存泄漏?【热度: 748】【工程化】

关键词:内存机制、内存泄露

关键词:内存机制、内存泄露

关键词:内存机制、内存泄露

一、V8 引擎内存管理机制概述

V8 是 Google 开发的 JavaScript 引擎,采用自动垃圾回收机制管理内存,其核心流程包括:

1. 内存分配
  • 栈内存:存储原始类型值(如 NumberStringBoolean)和函数调用栈,由引擎自动分配/释放。
  • 堆内存:存储引用类型值(如 ObjectArrayFunction),需手动分配(通过 new 等操作),由垃圾回收器自动回收。
2. 垃圾回收(GC)机制

V8 使用分代回收策略,将堆内存分为新生代老生代,针对不同生命周期的对象采用不同回收算法:

  • 新生代(小内存空间,存活时间短)
    • 算法Scavenge(复制算法)。
    • 流程:将内存分为 FromTo 两个区域,存活对象从 From 复制到 To,清空 From 并交换区域角色。
    • 适用场景:临时变量、函数作用域内的对象。
  • 老生代(大内存空间,存活时间长)
    • 算法Mark-Sweep(标记-清除)和 Mark-Compact(标记-整理)结合。
    • 流程
      1. 标记:遍历所有可达对象并标记为存活。
      2. 清除:删除未标记的对象,回收内存。
      3. 整理:移动存活对象,压缩内存空间,避免碎片。
    • 适用场景:全局对象、闭包引用的对象。

二、内存泄漏的常见原因

内存泄漏指不再使用的对象因被错误引用而无法被 GC 回收,常见场景包括:

  1. 闭包不当使用:内部函数引用外部变量,导致变量无法释放。
  2. 全局变量泄漏:意外创建全局变量(如未声明直接赋值)。
  3. DOM 引用泄漏:DOM 对象与 JavaScript 对象形成循环引用(如 element.onclick = element)。
  4. 定时器未清除setInterval/setTimeout 创建的回调函数未及时取消。
  5. 循环引用:对象间相互引用(如 obj.a = obj.b; obj.b = obj.a)。

三、通过优化闭包减少内存泄漏

1. 避免不必要的闭包
  • 问题:嵌套函数过度引用外部作用域变量,导致变量常驻堆内存。
    function outer() {
      const largeData = new Array(1000000).fill(1); // 大数组
      function inner() {
        // 仅使用部分数据时,仍引用整个 largeData
        return largeData.slice(0, 10);
      }
      return inner; // 闭包持有 largeData 引用
    }
    const fn = outer(); // largeData 无法释放
    
  • 优化:仅传递闭包需要的变量,避免引用整个对象。
    function outer() {
      const largeData = new Array(1000000).fill(1);
      const neededData = largeData.slice(0, 10); // 提取必要数据
      function inner() {
        return neededData; // 闭包仅引用 small data
      }
      return inner;
    }
    
2. 及时释放闭包引用
  • 问题:闭包引用的变量在不再使用时未被解除引用。
    let globalFn = null;
    function createClosure() {
      const obj = { key: "value" };
      globalFn = function () {
        return obj; // 闭包引用 obj
      };
    }
    createClosure();
    // 后续不再需要 globalFn 时,未置为 null
    
  • 优化:不再使用闭包时,手动解除引用。
    let globalFn = null;
    function createClosure() {
      const obj = { key: "value" };
      globalFn = function () {
        return obj;
      };
    }
    createClosure();
    // 释放闭包
    globalFn = null; // obj 失去引用,可被 GC 回收
    
3. 使用弱引用(WeakMap/WeakSet)
  • 场景:闭包需缓存对象,但不希望阻止其回收。
    const cache = new WeakMap(); // 弱引用 map
    function outer(obj) {
      cache.set(obj, function () {
        // 闭包引用 obj,但 WeakMap 不阻止 obj 被回收
        return obj.property;
      });
      return cache.get(obj);
    }
    
  • 原理WeakMap 的键为弱引用,若对象无其他引用则会被回收,闭包自动失效。

四、通过优化对象结构减少内存泄漏

1. 避免循环引用
  • 问题:对象间相互引用导致 GC 无法回收。
    function createCycle() {
      const a = { name: "a" };
      const b = { name: "b" };
      a.ref = b; // a 引用 b
      b.ref = a; // b 引用 a(循环引用)
    }
    createCycle(); // a 和 b 无法被回收
    
  • 优化:手动断开循环引用。
    function createCycle() {
      const a = { name: "a" };
      const b = { name: "b" };
      a.ref = b;
      b.ref = a;
      // 使用完毕后断开引用
      a.ref = null;
      b.ref = null;
    }
    
2. 减少不必要的属性引用
  • 问题:对象属性引用大型数据或全局对象。
    const globalData = { largeArray: new Array(1000000).fill(1) };
    function createObject() {
      return {
        data: globalData, // 引用全局大型对象
        method: function () {
          /* 使用 data */
        },
      };
    }
    const obj = createObject();
    // 即使不再使用 obj.data,globalData 仍被引用
    
  • 优化:仅在需要时传递数据副本或弱引用。
    const globalData = { largeArray: new Array(1000000).fill(1) };
    function createObject() {
      // 传递副本而非原对象(适用于不可变数据)
      return {
        data: { ...globalData }, // 浅拷贝,减少引用
        method: function () {
          /* 使用 data */
        },
      };
    }
    
3. 合理使用对象池(Object Pooling)
  • 场景:频繁创建/销毁大型对象时,复用对象可减少内存分配/回收压力。
    const objectPool = [];
    function createObject() {
      if (objectPool.length > 0) {
        return objectPool.pop(); // 复用池中的对象
      }
      return { data: new Array(1000000).fill(1) }; // 新建对象
    }
    function destroyObject(obj) {
      obj.data.length = 0; // 清理数据
      objectPool.push(obj); // 放回对象池
    }
    
  • 注意:对象池需配合引用计数或手动管理,避免无效对象残留。

五、内存泄漏检测工具

  1. Chrome DevTools
    • Memory 面板:录制内存快照,对比不同时刻的对象引用,定位泄漏对象。
    • Performance 面板:分析内存分配趋势,识别频繁创建的未释放对象。
  2. Node.js 工具
    • process.memoryUsage():监控堆内存使用情况。
    • --expose-gc 标志:手动触发 GC,配合 console.log 调试。

总结

优化内存管理的核心原则是:减少不必要的引用,及时释放不再使用的对象。通过合理设计闭包作用域、避免循环引用、使用弱引用和对象池等策略,可有效降低内存泄漏风险。同时,结合浏览器或 Node.js 提供的调试工具,定期分析内存快照,是定位和解决泄漏问题的关键。

1112. Web Components 有哪些优势【热度: 489】【工程化】

关键词:Web Components

Web Components 作为现代前端开发的重要技术,具有以下显著优势:

一、真正的组件封装

  • 样式隔离
    Shadow DOM 确保组件内部样式不会泄露到外部,也不受外部样式影响,彻底解决 CSS 全局污染问题。
    示例:组件内部的 .button { color: red } 不会影响外部按钮样式。

  • DOM 封装
    组件内部结构对外部不可见,避免被意外修改,实现真正的关注点分离。
    对比:传统组件(如 React/Vue)仍依赖全局 DOM 结构。

二、原生浏览器支持

  • 无需框架依赖
    作为浏览器原生标准(如 Chrome、Firefox、Safari 均支持),可直接在任何环境使用,降低技术栈复杂度。
    场景:在 legacy 项目或多框架共存环境中复用组件。

  • 轻量级
    相比框架组件(如 React 组件需引入 React 库),Web Components 更轻量,适合性能敏感场景。

三、跨框架兼容性

  • 真正的“一次编写,到处运行”
    可在 React、Vue、Angular 等任何框架中无缝集成,甚至可用于无框架的原生项目。
    示例
    <!-- 在 Vue 项目中使用 Web Components -->
    <custom-button @click="handleClick"></custom-button>
    

四、高度可复用性

  • 标准化组件格式
    基于 HTML、CSS、JS 标准,无需学习特定框架语法,降低开发者学习成本。
    生态:可复用现有 HTML 组件生态(如 Material Design Web Components)。

  • 独立分发
    可打包为独立文件(如 .js),通过 CDN 直接引入,无需复杂构建流程。
    示例

    <script src="https://cdn.example.com/custom-button.js"></script>
    

五、渐进式增强友好

  • 支持降级体验
    组件可先提供基础功能(Light DOM),再通过 JS 增强(Shadow DOM),确保低 JS 环境下仍可用。
    示例
    <custom-form>
      <form>
        <!-- 基础表单内容 -->
      </form>
    </custom-form>
    

六、未来兼容性

  • W3C 标准演进
    作为浏览器原生标准,长期维护性更强,减少技术栈过时风险。
    对比:第三方框架(如 jQuery、Backbone)可能随时间淘汰。

七、性能优化

  • 浏览器级优化
    原生组件渲染效率更高,尤其在大规模列表渲染时(如 1000+ 组件),性能优于虚拟 DOM 框架。

  • 按需加载
    通过 <script type="module"> 和动态导入,可实现组件的懒加载。
    示例

    import("./heavy-component.js").then(() => {
      document.body.innerHTML += "<heavy-component></heavy-component>";
    });
    

八、简化团队协作

  • 标准化接口
    通过自定义属性(Attributes)和事件(Events)定义清晰的组件接口,降低团队沟通成本。
    示例
    <custom-slider min="0" max="100" value="50" @change="updateValue"></custom-slider>
    

九、与现有技术互补

  • 框架集成
    主流框架(如 React、Vue)均提供官方支持 Web Components 的方式。
    React 示例

    function App() {
      return <custom-element some-prop="value" />;
    }
    
  • 微前端场景
    作为微前端架构中的“原子组件”,实现跨应用复用。

十、降低技术债务

  • 独立升级
    组件可独立于应用升级,无需重构整个项目。
    场景:将 legacy 项目逐步迁移至现代架构。

应用场景举例

  1. 企业级组件库(如 Ant Design、Element UI 的 Web Components 版本)
  2. 跨部门复用组件(如 Header、Footer、Toast 等基础组件)
  3. 第三方插件集成(如广告组件、评论系统)
  4. 低代码平台(通过标准组件降低用户学习成本)

总结

Web Components 凭借原生支持、真正封装、跨框架兼容三大核心优势,成为构建未来前端应用的理想选择。尤其适合需要长期维护、多团队协作、跨技术栈集成的大型项目。随着浏览器兼容性的提升(当前支持率约 95%),其应用场景将越来越广泛。

1113. 为何 Web Components 没有成为 web 前端的主流技术框架, 反而是 react 和 vue 呢?【热度: 531】【工程化】

关键词:Web Components

Web Components 虽具备技术优势,但未能成为前端主流框架的核心原因可从以下维度分析:

一、生态与工具链成熟度

  • React/Vue 的生态优势
    主流框架拥有完善的工具链(如 Webpack、Vite)、状态管理库(Redux、Pinia)、UI 组件库(Ant Design、Element),以及丰富的文档和社区支持。
    对比:Web Components 的生态碎片化严重,缺乏统一的最佳实践。

  • 框架集成成本
    在已有项目中引入 Web Components 可能需修改构建流程,而 React/Vue 可无缝集成现有工具链。
    示例:Vue 组件可直接使用 <script setup> 语法,Web Components 则需手动处理生命周期。

二、开发体验与抽象层级

  • 声明式 vs 命令式
    React/Vue 通过 JSX/模板语法提供更高级的抽象,降低 DOM 操作复杂度。
    示例:React 的 useState 钩子比 Web Components 的 attributeChangedCallback 更直观。

  • 状态管理复杂度
    Web Components 原生未提供状态管理方案,处理复杂数据流需自行实现或引入第三方库(如 Redux),而 React/Vue 内置状态管理机制。

三、学习曲线与开发者偏好

  • 入门门槛
    React/Vue 的概念(如虚拟 DOM、组件化)更贴近现代前端思维,而 Web Components 需掌握 Shadow DOM、Custom Elements 等多个低阶 API。
    数据:Stack Overflow 调查显示,React/Vue 的问题活跃度远高于 Web Components。

  • 框架黏性
    开发者倾向使用已熟悉的框架(如 React 开发者更愿用 React 生态组件),而非切换技术栈。

四、性能与优化难度

  • 虚拟 DOM 的优势
    React/Vue 通过虚拟 DOM 差异更新减少真实 DOM 操作,在复杂 UI 场景下性能更优。
    测试:大型列表渲染中,React 的 shouldComponentUpdate 比 Web Components 的原生更新更高效。

  • 优化工具缺失
    React/Vue 提供 Hooks、Suspense 等优化工具,Web Components 需手动实现类似功能。

五、浏览器兼容性与 Polyfill

  • 兼容性成本
    Web Components 在旧版浏览器(如 IE11)需引入大型 Polyfill,导致包体积膨胀。
    数据:核心 Polyfill 约增加 100KB 体积,而 React 压缩后约 42KB。

  • 特性碎片化
    不同浏览器对 Shadow DOM 的实现存在细微差异(如 CSS 变量支持),增加测试成本。

六、框架厂商推动与社区效应

  • 商业公司背书
    React(Meta)、Vue(开源但有官方支持)受益于大厂资源投入,持续迭代并推广新特性。
    对比:Web Components 作为 W3C 标准,缺乏单一实体推动生态建设。

  • 社区驱动创新
    React/Vue 的第三方插件(如 React Query、Vue Router)加速了其普及,而 Web Components 社区活跃度较低。

七、适用场景差异

  • Web Components 的定位
    更适合构建原子组件(如按钮、输入框)或跨框架共享组件,而非完整应用框架。
    案例:Google 的 Material Web Components 作为独立组件库使用。

  • 框架的端到端解决方案
    React/Vue 提供路由、状态管理、构建工具等一站式解决方案,降低全栈开发复杂度。

八、历史机遇与开发者惯性

  • 时间窗口错位
    React(2013)、Vue(2014)诞生于前端工程化需求爆发期,而 Web Components 标准成熟较晚(2019 年全面支持)。

  • 技术选型惯性
    企业更倾向选择经过验证的框架(如 React),而非实验性技术。

九、设计哲学差异

  • 标准化 vs 灵活性
    Web Components 追求标准化,而 React/Vue 允许灵活选择技术栈(如状态管理方案)。
    争议:Web Components 的 Shadow DOM 完全隔离样式,导致某些场景(如全局主题)难以实现。

十、未来趋势:互补而非替代

  • 混合架构兴起
    现代项目常采用“框架 + Web Components”的混合模式(如在 React 中使用 Web Components 实现跨团队组件)。

  • 框架原生支持
    React 18+、Vue 3 均提供更好的 Web Components 集成方案,表明两者正走向融合而非竞争。

总结

Web Components 未成为主流的核心矛盾在于:标准化的底层技术 vs 开发者对高效工具链和抽象层级的需求。未来更可能作为基础设施(如组件通信标准)而非独立框架存在,与 React、Vue 等上层框架形成互补生态。

1118. 很多web前端框架里面会有约定式路由, 他们是如何实现的【热度: 331】【工程化】【出题公司: 阿里巴巴】

关键词:约定是路由

约定式路由(Convention over Configuration,CoC)是现代前端框架(如 Next.js、Nuxt.js、VitePress 等)广泛采用的路由实现方式,其核心思想是根据文件目录结构自动生成路由配置,无需手动编写冗长的路由表。下面介绍其实现原理和关键技术:

一、核心原理

约定式路由通过以下步骤工作:

  1. 文件系统扫描:框架在构建或运行时遍历指定目录(如pages/),获取所有文件和文件夹结构。
  2. 路径映射规则:将文件路径转换为路由路径,例如:
    • pages/index.js/
    • pages/posts/[id].js/posts/:id(动态路由)
  3. 路由配置生成:根据映射规则生成路由配置对象(如 React Router 或 Vue Router 所需的格式)。
  4. 运行时匹配:在用户访问时,根据 URL 匹配对应的组件。

二、关键实现细节

1. 文件系统扫描与路径解析

框架使用 Node.js 的fs模块读取文件目录,并递归生成路径树。例如:

// 简化的文件扫描逻辑
import fs from "fs";
import path from "path";

function scanPages(dir, basePath = "") {
  const entries = fs.readdirSync(dir, { withFileTypes: true });
  const routes = [];

  for (const entry of entries) {
    const filePath = path.join(dir, entry.name);
    const routePath = path.join(basePath, entry.name);

    if (entry.isDirectory()) {
      // 递归扫描子目录
      routes.push(...scanPages(filePath, routePath));
    } else {
      // 处理文件(如.js、.vue)
      routes.push({
        file: filePath,
        path: convertToRoutePath(routePath), // 转换为路由路径
      });
    }
  }

  return routes;
}

// 路径转换示例:pages/posts/[id].js → /posts/:id
function convertToRoutePath(filePath) {
  // 移除扩展名
  let route = filePath.replace(/\.(js|jsx|ts|tsx|vue)$/, "");

  // 处理动态路由:[id] → :id
  route = route.replace(/\[([^\]]+)\]/g, ":$1");

  // 处理索引文件:index → /
  route = route.replace(/\/index$/, "");

  // 确保以斜杠开头
  return route.startsWith("/") ? route : `/${route}`;
}
2. 动态路由与嵌套路由
  • 动态路由:使用方括号[]表示参数,例如:

    • pages/users/[id].js → 匹配/users/123
    • pages/[...all].js → 匹配所有路径(通配符路由)
  • 嵌套路由:通过目录结构实现,例如:

    pages/
      posts/
        index.js    → /posts
        [id]/
          index.js  → /posts/:id
          comments/
            index.js → /posts/:id/comments
    
3. 路由配置生成

将扫描结果转换为框架所需的路由配置格式。例如,为 React Router 生成配置:

// 生成React Router配置
function generateReactRoutes(pages) {
  return pages.map((page) => ({
    path: page.path,
    element: () => import(`./pages/${page.file}`), // 动态导入组件
  }));
}

// 使用生成的路由配置
const router = createBrowserRouter(generateReactRoutes(pages));
4. 特殊文件处理
  • 布局文件:如_layout.jslayout.vue,用于包裹子路由:

    pages/
      _layout.js    → 所有页面共用布局
      index.js      → 使用_layout.js的布局
    
  • 错误页面:如404.jserror.vue,处理未匹配的路由:

    // 404页面自动映射到未匹配的路由
    {
      path: '*',
      element: <NotFoundPage />
    }
    
5. 运行时优化
  • 按需加载:使用动态导入(import())实现组件懒加载。
  • 路由预取:在用户可能访问的链接上预加载组件(如 Next.js 的next/link)。
  • 缓存机制:开发环境中缓存扫描结果,仅在文件变化时重新生成路由。

三、不同框架的实现差异

框架 约定规则 实现特点
Next.js pages/目录,支持[param]动态路由 服务端渲染(SSR)支持、自动代码分割
Nuxt.js pages/目录,支持_param动态路由 基于 Vue Router,支持中间件
VitePress docs/目录,Markdown 文件自动转换 静态网站生成(SSG),支持 Vue 组件

四、优缺点

优点
  • 减少样板代码:无需手动维护路由配置。
  • 提高一致性:文件结构即路由结构,直观易懂。
  • 易于扩展:新增页面只需添加文件,无需修改路由表。
缺点
  • 灵活性受限:复杂路由模式可能需要额外配置。
  • 学习成本:需要熟悉框架的约定规则。
  • 性能开销:大型项目中扫描文件系统可能影响构建速度。

五、手动实现简易版约定式路由

以下是一个简化的实现示例,用于理解核心逻辑:

// 简易约定式路由实现
import fs from "fs";
import path from "path";
import { createRouter, createWebHistory } from "vue-router";

// 扫描pages目录
const pagesDir = path.resolve(__dirname, "pages");
const routes = fs
  .readdirSync(pagesDir)
  .filter((file) => file.endsWith(".vue"))
  .map((file) => {
    const name = file.replace(/\.vue$/, "");
    const path = name === "index" ? "/" : `/${name}`;

    return {
      path,
      component: () => import(`./pages/${file}`),
    };
  });

// 创建路由实例
const router = createRouter({
  history: createWebHistory(),
  routes,
});

export default router;

六、总结

约定式路由通过文件系统映射自动化配置,极大简化了路由管理。其核心在于扫描文件、转换路径、生成配置和运行时匹配。现代框架在此基础上添加了动态路由、嵌套路由、懒加载等高级特性,提升了开发体验和应用性能。

1119. 如何对一个大型 SPA(单页应用)进行全面的性能优化?请从构建阶段、运行时、网络请求、渲染等多个维度说明。【热度: 761】【工程化】【出题公司: 腾讯】

关键词:性能优化

对大型 SPA(单页应用)进行全面性能优化需要从多个维度入手,以下是系统性的优化策略:

一、构建阶段优化

1. 代码分割与懒加载
  • 动态导入(Dynamic Import):按需加载路由组件、组件库、第三方模块

    // React示例:懒加载路由组件
    const HomePage = React.lazy(() => import("./pages/HomePage"));
    
    // Vue示例:异步组件
    const HomePage = () => import("./pages/HomePage.vue");
    
  • 路由级分割:按路由拆分 chunks,减少首屏加载体积

  • 组件级分割:对大型组件(如数据表格、图表)单独拆分

2. Tree Shaking
  • 启用 ESModule + 生产环境配置,移除未使用的代码
  • 优化第三方库:选择支持 Tree Shaking 的库(如 Lodash-es)
3. 压缩与混淆
  • 使用 Terser 压缩 JS,cssnano 压缩 CSS
  • 移除调试代码:console.logdebugger
4. 资源预加载/预取
  • 通过 HTML 标签声明预加载关键资源
    <link rel="preload" href="critical.js" as="script" /> <link rel="prefetch" href="non-critical.js" as="script" />
    
  • 框架集成:如 Next.js 的next/link自动预取

二、运行时优化

1. 虚拟列表(Virtual List)
  • 只渲染可视区域内的列表项,大幅减少 DOM 节点数量
  • 库推荐:react-window(React)、vue-virtual-scroller(Vue)
2. 防抖(Debounce)与节流(Throttle)
  • 高频事件(如滚动、窗口 resize)处理优化
    // 防抖示例
    const debouncedHandleScroll = debounce(handleScroll, 300);
    window.addEventListener("scroll", debouncedHandleScroll);
    
3. 状态管理优化
  • 避免全局状态滥用,使用局部状态(如 React 的 useState)
  • 不可变数据结构:使用 Immer 简化不可变数据操作
  • 状态分片:按功能拆分 store(如 Redux Toolkit 的 slice)
4. 内存管理
  • 避免内存泄漏:及时清理定时器、事件监听器
  • 使用 WeakMap/WeakSet 存储临时引用

三、网络请求优化

1. 缓存策略
  • HTTP 缓存:合理设置Cache-ControlETag
  • 客户端缓存:使用localStorageIndexedDB缓存静态数据
  • Service Worker:实现离线缓存和请求拦截
2. 资源加载优化
  • 图片优化:使用 WebP/AVIF 格式、响应式图片(srcset)
    <img src="image.jpg" srcset="image.jpg 1x, image@2x.jpg 2x" loading="lazy" alt="Description" />
    
  • 按需加载字体:使用font-display: swap避免 FOUT
3. API 请求优化
  • 合并请求:将多个小请求合并为批量请求
  • 缓存失效策略:使用 SWR(Stale-While-Revalidate)模式
  • 服务端数据预取:如 Next.js 的getServerSideProps

四、渲染优化

1. 减少重排(Layout)与重绘(Paint)
  • 批量修改 DOM:使用 DocumentFragment
  • 避免强制同步布局(Force Synchronous Layout)
  • 使用transformopacity进行动画,利用合成层(Compositing)
2. 懒加载(Lazy Loading)
  • 图片懒加载:浏览器原生支持(loading="lazy"
  • 组件懒加载:结合 Intersection Observer API 实现可视区域加载
3. 服务端渲染(SSR)/静态站点生成(SSG)
  • 首屏 HTML 直出,减少客户端渲染时间
  • 框架支持:Next.js(React)、Nuxt.js(Vue)
4. 减少包体积
  • 移除不必要的依赖
  • 使用 CDN 加载第三方库:
    <script src="https://cdn.tailwindcss.com"></script>
    

五、工具与监控

1. 性能分析工具
  • Chrome DevTools:Lighthouse、Performance 面板
  • WebPageTest:多地点性能测试
  • 框架专用工具:React DevTools 的 Profiler
2. 持续监控
  • 埋点:记录关键指标(FP、FCP、LCP、TTFB)
  • 告警:设置性能阈值,异常时自动通知

六、框架特定优化

React
  • 使用React.memouseMemouseCallback避免不必要渲染
  • 使用 Concurrent Mode(并发模式)提高响应性
Vue
  • 使用v-once渲染静态内容
  • 使用v-memo缓存组件树

七、总结

大型 SPA 性能优化需遵循以下原则:

  1. 先测量,后优化:使用工具定位瓶颈点

  2. 从大到小:优先处理首屏加载、关键路径

  3. 分层优化:构建、网络、运行时、渲染各维度协同

  4. 持续监控:建立性能基线,防止退化

资深开发者相关问题【共计 2 道题】

1120. 微前端架构有哪些主流框架可以选, 各有啥优劣势?【热度: 554】【工程化】【出题公司: 腾讯】

关键词:微前端框架

以下是前端领域主流微前端框架的深度解析,涵盖核心特性、优劣势及适用场景,结合最新技术动态和企业级实践:

一、核心框架对比与选型指南

1. Qiankun(蚂蚁集团)
  • 核心特性
    • 基于 Single-SPA 封装,支持 React/Vue/Angular 等多框架共存
    • 提供 JS 沙箱(Proxy/快照机制)和 CSS 沙箱(Shadow DOM/动态作用域)
    • 完整的生命周期管理和路由劫持机制
  • 优势
    • 成熟稳定,经过蚂蚁金服大规模生产验证
    • 开箱即用,配置简单,适合快速搭建微前端架构
    • 完善的生态支持(如 Vue CLI 插件、Webpack 工具链)
  • 劣势
    • 对子应用侵入性较强,需改造入口文件和构建配置
    • 沙箱机制在复杂场景下可能存在性能损耗
  • 适用场景
    • 大型企业级应用,技术栈多样且需长期维护
    • 需统一路由管理和状态共享的中台项目
2. Single-SPA(独立开源)
  • 核心特性
    • 微前端底层框架,提供应用加载、路由调度和生命周期管理
    • 高度灵活,可与任意框架结合
    • 支持应用预加载和资源共享
  • 优势
    • 无框架绑定,适合自定义程度高的复杂场景
    • 社区活跃,插件生态丰富(如 single-spa-react/single-spa-vue)
  • 劣势
    • 配置繁琐,需手动处理样式隔离和通信机制
    • 对新手不够友好,学习曲线陡峭
  • 适用场景
    • 技术栈混合且需高度定制的项目
    • 已有成熟路由和状态管理体系的应用改造
3. Module Federation(Webpack 5 原生功能)
  • 核心特性
    • 基于 Webpack 5 的模块共享机制,支持跨应用动态加载模块
    • 天然支持依赖共享,减少重复打包
    • 与 Webpack 深度集成,开发体验一致
  • 优势
    • 模块级共享,代码复用率高
    • 按需加载提升性能,适合大型应用
  • 劣势
    • 强依赖 Webpack 5,构建配置复杂
    • 缺乏完整的微前端生态(如路由、通信需自行实现)
  • 适用场景
    • 技术栈统一且使用 Webpack 5 的项目
    • 需要共享组件库或基础模块的多团队协作
4. MicroApp(京东)
  • 核心特性
    • 基于类 Web Components 实现,子应用零改造接入
    • 提供 JS 沙箱(Proxy)和样式隔离(Shadow DOM)
    • 支持虚拟路由系统和跨框架通信
  • 优势
    • 低侵入性,子应用只需配置跨域即可接入
    • 高性能沙箱机制,支持多实例场景
  • 劣势
    • 生态较小,社区支持有限
    • Proxy 沙箱在 IE 等旧浏览器中不兼容
  • 适用场景
    • 快速集成现有项目,尤其是技术栈混杂的遗留系统
    • 对性能和隔离性要求较高的中大型应用
5. Wujie(腾讯)
  • 核心特性
    • 结合 Web Components 和 iframe,实现原生隔离
    • 支持样式、JS、路由全隔离,安全性高
    • 提供轻量级通信机制(postMessage/自定义事件)
  • 优势
    • 天然隔离性,适合金融、医疗等高安全场景
    • 高性能按需加载,首屏时间优化显著
  • 劣势
    • iframe 的历史包袱(如滚动条、SEO 问题)
    • Web Components 兼容性问题(IE11 不支持)
  • 适用场景
    • 对隔离性和安全性要求极高的场景
    • 技术栈统一且现代浏览器占比高的项目
6. Garfish(字节跳动)
  • 核心特性
    • 基于 Proxy 沙箱和动态样式隔离,支持多实例
    • 提供跨框架通信和状态管理工具链
    • 集成 Vite 和 Webpack,构建灵活
  • 优势
    • 高效资源管理,支持并行加载和缓存优化
    • 强大的扩展性,适合复杂前端生态
  • 劣势
    • 文档和社区活跃度待提升
    • 对构建工具链的整合需一定学习成本
  • 适用场景
    • 大型应用跨团队协作,需高效资源调度
    • 技术栈混合且追求性能的互联网产品
7. ICestark(阿里巴巴)
  • 核心特性
    • 基于 qiankun 和 Web Components,支持多端(Web/小程序)
    • 强调状态管理和模块化,提供全局状态总线
    • 支持服务端渲染(SSR)和静态站点生成(SSG)
  • 优势
    • 企业级解决方案,适合复杂业务场景
    • 完善的状态管理和跨应用通信机制
  • 劣势
    • 配置复杂,学习曲线陡峭
    • 对 SSR 和 SSG 的支持需额外配置
  • 适用场景
    • 大型企业级多端应用,需统一状态管理
    • 对 SSR 和 SEO 有强需求的项目
8. Piral(独立开源)
  • 核心特性
    • 基于插件机制,支持动态加载微应用模块
    • 提供可视化插件市场和 CLI 工具链
    • 支持混合技术栈和渐进式集成
  • 优势
    • 高度可扩展,适合插件化开发模式
    • 低侵入性,子应用可独立开发和部署
  • 劣势
    • 生态较小,中文资料较少
    • 对复杂路由和状态管理支持较弱
  • 适用场景
    • 插件化架构和快速迭代的创新项目
    • 团队熟悉 React 或 Vue 的中小型应用

二、关键维度对比与选型建议

1. 技术栈兼容性
  • 多框架支持:Qiankun > Single-SPA > Garfish > ICestark
  • 技术栈无关:MicroApp > Wujie > Module Federation
  • 推荐场景:若存在 React/Vue/Angular 混合开发,优先选择 Qiankun 或 Single-SPA;若需完全技术栈无关,MicroApp 和 Wujie 更优。
2. 隔离性与安全性
  • 强隔离:Wujie(iframe+Shadow DOM)> MicroApp(Proxy 沙箱)> Qiankun(Proxy/快照沙箱)
  • 弱隔离:Module Federation(依赖 Webpack 模块作用域)
  • 推荐场景:金融、医疗等高安全场景选择 Wujie;普通业务场景 Qiankun 或 MicroApp 即可。
3. 性能与资源管理
  • 高性能:Module Federation(模块级按需加载)> Garfish(并行加载优化)> MicroApp(轻量级沙箱)
  • 低性能:Qiankun(沙箱开销)> Single-SPA(手动优化要求高)
  • 推荐场景:追求极致性能选择 Module Federation 或 Garfish;中大型应用可平衡 Qiankun 的成熟度与性能。
4. 开发体验与学习成本
  • 低学习成本:Qiankun > MicroApp > Wujie
  • 高学习成本:Module Federation > Single-SPA > ICestark
  • 推荐场景:新手或快速交付项目选择 Qiankun 或 MicroApp;复杂场景需深入学习 Single-SPA 或 Module Federation。
5. 生态与社区支持
  • 成熟生态:Qiankun > Single-SPA > Module Federation
  • 新兴生态:MicroApp > Wujie > Garfish
  • 推荐场景:长期维护项目选择 Qiankun 或 Single-SPA;创新项目可尝试 MicroApp 或 Garfish。

三、落地避坑指南

1. 样式隔离方案选择
  • Shadow DOM:适合现代浏览器环境,需处理弹窗组件挂载问题
  • 动态样式作用域:兼容性好,需监控动态插入样式
  • 推荐实践:默认启用 Qiankun 的experimentalStyleIsolation,关键子应用逐步迁移至 Shadow DOM
2. 通信机制设计
  • 轻量级通信:使用框架内置事件总线(如 Qiankun 的props传递)
  • 复杂通信:结合状态管理库(如 Redux)或微服务 API
  • 避坑点:避免直接操作全局变量,优先使用框架提供的通信接口
3. 路由管理策略
  • 主应用统一管理:适合单页应用模式,需处理子应用路由前缀
  • 子应用自治:适合多页应用模式,需注意路由冲突
  • 推荐实践:使用 Qiankun 的activeRule或 Single-SPA 的registerApplication配置路由匹配规则
4. 资源加载优化
  • 预加载:Qiankun 的preload配置或 Webpack 的prefetch注释
  • 按需加载:Module Federation 的动态导入或 Garfish 的并行加载机制
  • 避坑点:避免同时加载过多子应用,优先加载关键路径资源

四、总结与趋势展望

1. 框架选型决策树
  • 技术栈多样 → Qiankun 或 Single-SPA
  • 高隔离需求 → Wujie 或 MicroApp
  • 模块共享优先 → Module Federation 或 EMP
  • 快速集成 → MicroApp 或 Piral
  • 企业级复杂场景 → ICestark 或 Garfish
2. 未来趋势
  • Web Components 普及:Wujie、MicroApp 等框架将更受青睐
  • 构建工具链整合:Vite+Module Federation 模式(如 EMP)可能成为主流
  • 全栈微前端:ICestark 等框架向多端(Web/小程序/Node)扩展

通过综合评估项目需求、技术栈现状和团队能力,选择最适合的微前端框架,并结合上述避坑指南,可有效降低集成成本,提升系统可维护性和扩展性。

1121. 如何将一个非常到大的 spa 应用, 迁移到微前端架构, 有哪些考虑因素【热度: 754】【工程化】

关键词:微前端框架

将大型 SPA(单页应用)迁移到微前端架构是一个复杂的系统工程,需要从业务、技术、团队等多维度综合考量。其核心目标是解决大型 SPA 的代码臃肿、团队协作低效、技术栈锁定、部署缓慢等问题,同时确保迁移过程平稳、业务不受影响。

一、迁移前的核心前提:明确目标与现状评估

在动手迁移前,需先明确“为什么要做微前端”,避免为了技术而技术。同时,需全面评估现有 SPA 的现状,为迁移策略提供依据。

1. 明确迁移目标与价值

微前端的核心价值是**“去中心化的前端架构”**,迁移目标应围绕以下几点展开:

  • 团队自治:让不同团队(如商品、订单、支付团队)独立开发、测试、部署各自负责的模块,减少跨团队协作成本。
  • 技术栈灵活:允许不同微应用使用不同技术栈(如老模块用 Vue2,新模块用 React),避免技术栈锁定,支持增量升级。
  • 独立部署:单个微应用的更新无需全量发布整个应用,缩短发布周期,降低部署风险。
  • 故障隔离:单个微应用崩溃不影响其他模块,提高系统稳定性。

若现有 SPA 未遇到上述问题(如团队小、业务简单),则无需迁移。

2. 评估现有 SPA 的现状

需深入分析现有应用的“痛点”和“基础”,避免盲目迁移:

  • 代码结构:是否有清晰的业务模块边界?模块间耦合度如何(如是否大量使用全局变量、公共函数)?是否存在“牵一发而动全身”的依赖?
  • 技术栈:当前使用的框架(如 React、Vue)、构建工具(Webpack、Vite)、状态管理方案(Redux、Vuex)等,是否存在升级困难(如老项目用 jQuery,难以维护)?
  • 团队结构:现有团队是按技术分层(如 UI 组、接口组)还是按业务域划分?团队协作是否存在频繁冲突(如代码合并冲突、发布阻塞)?
  • 性能与稳定性:现有 SPA 的首屏加载时间、交互响应速度、崩溃率等指标如何?迁移后需确保这些指标不下降。

二、微前端架构的核心设计要素

迁移的核心是设计一套符合业务的微前端架构,需重点解决“微应用如何拆分、如何协作、如何集成”三大问题。

1. 微应用的拆分策略:高内聚、低耦合

微应用的拆分是迁移的“灵魂”,直接决定后续协作效率和维护成本。拆分需遵循**“业务域边界清晰”**原则,常见拆分方式:

拆分维度 适用场景 示例(电商场景)
按业务域拆分 业务模块独立性强,有明确的“职责范围” 商品模块(列表、详情)、订单模块、支付模块
按用户角色拆分 不同角色使用的功能差异大(如 C 端用户、B 端商家) 买家端微应用、商家端微应用
按功能层级拆分 功能有明显的“上下层”关系(如基础组件、业务组件) 公共组件微应用、核心业务微应用

拆分原则

  • 每个微应用需有独立的业务闭环(如“订单模块”可独立完成下单、支付、退款流程),避免跨应用依赖。
  • 尽量减少“跨微应用调用”(如 A 微应用直接修改 B 微应用的 DOM 或状态),若必须调用,需通过标准化接口。
  • 拆分粒度不宜过细(避免微应用数量过多,增加管理成本),也不宜过粗(失去微前端的灵活性)。
2. 通信机制:微应用间的“对话规则”

微应用间需通信(如“商品详情页”跳转“订单页”时传递商品 ID),但需避免通信逻辑导致新的耦合。常见方案:

  • 发布-订阅模式(EventBus):通过全局事件总线传递消息(如 A 微应用触发addToCart事件,购物车微应用监听并处理)。适合简单、低频的通信(如跳转、数据传递)。
    优点:低耦合(无需知道对方存在);缺点:事件过多时难以追踪。

  • 公共状态服务:将全局共享状态(如用户信息、权限)放在独立的“状态服务”中(如用 Redis 或前端全局 Store),微应用通过 API 读写。适合高频、核心数据共享(如用户登录状态)。
    优点:状态统一;缺点:需设计状态更新规则(如防止并发修改冲突)。

  • 接口调用:微应用通过暴露“对外 API”供其他应用调用(如订单微应用提供createOrder(params)方法)。适合复杂交互(如跨应用提交数据)。
    优点:逻辑清晰;缺点:需维护 API 文档,耦合度略高。

原则:微应用内部状态(如表单临时数据)自行管理,仅将“必须共享”的数据放入全局通信层。

3. 路由管理:谁来“指挥”微应用加载?

微前端需一个“主应用(容器应用)”负责路由分发:根据 URL 匹配对应的微应用,并加载/卸载微应用。核心考虑点:

  • 路由规则设计:需避免微应用路由冲突(如 A 应用用/list,B 应用也用/list)。解决方案:为每个微应用分配“路由命名空间”(如商品应用路由前缀为/goods,订单应用为/order)。

  • 路由切换策略

    • 主应用监听路由变化,匹配到目标微应用后,动态加载其资源(JS/CSS)并挂载到 DOM;
    • 卸载当前微应用时,需清理其 DOM、事件监听、内存占用(避免内存泄漏)。
  • 框架选择:成熟的微前端框架(如 qiankun、single-spa)已内置路由管理能力,可直接复用(如 qiankun 通过registerMicroApps注册微应用与路由的映射关系)。

4. 隔离机制:避免“互相干扰”

大型 SPA 的常见问题是“全局污染”(如样式冲突、变量覆盖),微前端需通过隔离机制解决:

  • 样式隔离

    • Shadow DOM:将微应用的 DOM 放入 Shadow DOM 中(浏览器原生隔离),但可能影响全局样式(如 UI 组件库的全局主题),且部分浏览器兼容性有限;
    • CSS Modules/BEM 规范:微应用内的样式通过命名隔离(如用goods__title--active而非title);
    • Webpack 前缀:通过css-loader给微应用样式自动添加前缀(如#goods-app .title),确保样式仅作用于当前应用。
  • JS 隔离

    • 沙箱机制:主应用为每个微应用创建独立的 JS 执行环境(如 qiankun 的sandbox配置),避免全局变量(如window)被篡改;
    • 禁止直接操作全局对象:微应用需通过主应用提供的 API 访问全局资源(如window.localStorage需通过mainApp.storage.get()调用)。
5. 资源加载:性能与效率的平衡

微应用的资源(JS/CSS)加载直接影响首屏性能,需设计合理的加载策略:

  • 加载时机

    • 按需加载:仅当用户访问某路由时,才加载对应微应用的资源(适合非核心模块,如“帮助中心”);
    • 预加载:在空闲时间提前加载可能用到的微应用资源(如用户进入商品页后,预加载订单应用资源)。
  • 资源共享:避免重复加载公共依赖(如 React、Vue、UI 组件库):

    • 用 Webpack Module Federation 共享依赖(主应用或某个微应用作为“宿主”,其他应用复用其依赖);
    • 将公共资源放入 CDN,微应用通过externals配置引用,减少打包体积。
  • 缓存策略:对微应用资源(如 JS 包)设置合理的缓存过期时间(如Cache-Control: max-age=3600),配合版本号(如app.js?v=2.1.0)确保更新生效。

6. 状态管理:全局状态与局部状态的边界

大型 SPA 通常有全局状态(如用户信息、权限)和局部状态(如表单数据),微前端需明确两者的管理边界:

  • 全局状态:仅存放“跨微应用共享且稳定”的数据(如用户 ID、登录状态、全局主题),由主应用或独立的“状态服务”管理(如用 Redux Toolkit 或 Pinia 的“全局实例”)。
    注意:全局状态需精简,避免成为“状态黑洞”(所有状态都往里塞,导致维护困难)。

  • 局部状态:微应用内部的状态(如商品列表的筛选条件、订单表单的输入值)由自身管理(如 React 组件用useState,Vue 用reactive),不依赖外部。

三、迁移实施:增量迁移,平稳过渡

大型 SPA 无法“一刀切”迁移,需采用**“增量迁移”**策略:先搭建基础框架,再逐步替换老模块,同时保留老应用的可用性,直到完全迁移。

1. 迁移步骤(以“主应用+微应用”模式为例)
  1. 搭建主应用(容器)
    主应用负责路由管理、微应用加载、全局通信、样式/JS 隔离等核心能力。初期可基于成熟框架(如 qiankun)快速搭建,无需开发业务功能。

  2. 选择“试点微应用”
    优先迁移独立、非核心、改动少的模块(如“用户中心”“设置页”),验证架构可行性(如通信、隔离、部署是否符合预期)。避免先迁移核心模块(如“支付流程”),减少风险。

  3. 老应用与微应用共存
    主应用通过“路由转发”同时支持老 SPA 和新微应用:访问老路由(如/old/goods)时加载原 SPA 的对应模块;访问新路由(如/new/order)时加载新微应用。
    需开发“适配层”:将老 SPA 的全局变量、事件通过主应用的通信机制暴露给新微应用(如老 SPA 的userInfo通过EventBus传递给新应用)。

  4. 逐步迁移核心模块
    试点验证通过后,按业务优先级迁移核心模块(如商品、订单)。迁移时需先“解耦老代码”(如将老模块的全局依赖改为通过主应用 API 获取),再用新技术栈实现。

  5. 下线老 SPA
    当所有模块迁移完成后,逐步下线老 SPA 的路由,主应用完全接管所有业务。

2. 团队协作与组织调整

微前端的成功依赖“团队自治”,需同步调整团队结构(康威定律:系统设计反映组织架构):

  • 按微应用对应的业务域划分团队(如“商品团队”负责商品微应用的全生命周期),避免按技术分层(如“前端组”“后端组”)。
  • 明确团队职责:每个团队独立负责开发、测试、部署、监控,仅需遵守主应用的“接入规范”(如通信 API、路由命名)。

四、风险与应对策略

迁移过程中可能遇到多种风险,需提前预案:

风险类型 具体问题 应对策略
性能下降 首屏加载时间变长(多应用资源加载) 优化资源加载(预加载、共享依赖)、压缩包体积(Tree-Shaking)、监控性能指标(LCP、FID)
兼容性问题 微应用在低版本浏览器(如 IE)运行异常 提前确定兼容范围,用 Babel/PostCSS 转译代码,对不支持的 API(如 Shadow DOM)降级处理
调试困难 多应用嵌套导致错误定位难(如“哪个应用抛了错”) 集成统一监控工具(如 Sentry),在错误信息中添加微应用标识;开发环境用sourcemap定位源码
发布冲突 微应用独立部署导致版本兼容问题(如 A 应用依赖 B 应用 v1.0,B 应用已升级到 v2.0) 制定版本兼容规范(如语义化版本),通过灰度发布验证兼容性,主应用支持“回滚到旧版本”

总结

将大型 SPA 迁移到微前端的核心是“以业务为中心,增量推进,平衡灵活性与复杂度”。需重点考虑:

  • 微应用拆分是否符合业务边界;

  • 通信、路由、隔离机制是否清晰;

  • 迁移过程是否平稳(老应用与新应用共存);

  • 团队是否能适应自治协作模式。

一个免费的在线压缩网站超越了付费的压缩软件

作者 Simon_He
2025年7月6日 19:27

大家好,我是 Simon,最近写了一个在线的压缩库,他的效果真的很惊艳,比一些付费的软件表现还好。

起因是我吐槽了,我们网站很多图片使用太大了,每次渲染的很慢,我就说了,你压缩一下再传,他说懒的弄,后来我就分享了一个我的工具,然后他说我这个肯定不好用,因为压缩每一家都有独特的算法,开源的肯定比不上,然后我就结合了 vibe coding 优化了这个网站,支持目录上传、拖拽、等等一系列的方式,能够直接左右对比压缩前后的图片差距,压缩的大小百分比,以及压缩质量的控制,然后再次安利了他,结果吊打了他的付费软件🤣

提供了 B 站的 链接: www.bilibili.com/video/BV1sf…

然后还有在线体验网站: browser-compress-image.netlify.app/

还有开源的 Github 地址:github.com/Simon-He95/…

如果你有一些好的建议,或者一些很棒的开源压缩浏览器端的库推荐,可以集成进来使用

CSS常见问题深度解析与解决方案(第三波)

作者 前端微白
2025年7月6日 18:09

一、元素居中问题精要

1. 普通div居中

.center-div {
  width: 300px;
  height: 200px;
  
  /* Flexbox方案 - 现代浏览器首选 */
  display: flex;
  justify-content: center;
  align-items: center;

  /* Grid方案 - 最简方式 */
  display: grid;
  place-items: center;

  /* 传统方法 - 固定宽高 */
  position: relative;
  left: 50%; 
  top: 50%;
  transform: translate(-50%, -50%);
}

2. 浮动元素居中

.float-container::after {
  content: "";
  display: table;
  clear: both;
}

.float-center {
  float: left;
  position: relative;
  left: 50%;
  transform: translateX(-50%);
}

3. 绝对定位元素居中

.absolute-center {
  position: absolute;
  
  /* 视口居中 */
  top: 0; 
  right: 0; 
  bottom: 0; 
  left: 0;
  margin: auto;
  
  /* 已知尺寸居中 */
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  
  /* 容器内居中 */
  inset: 0;
  margin: auto;
}

二、动画与视觉细节

4. 动画最小时间间隔

推荐至少16ms(≈60帧/秒)
原因:显示器的刷新率通常为60Hz,每帧间隔16.67ms。超过这个时间会导致动画卡顿,低于这个时间浏览器无法渲染额外帧。

/* 流畅动画的最佳实践 */
.animate {
  transition: transform 0.16s ease-out;
  animation: pulse 1s infinite;
}

5. position定位详解

absolute与fixed异同:

特性 absolute fixed
定位基准 最近的positioned祖先 视口(viewport)
滚动影响 随文档流滚动 始终固定在屏幕指定位置
共同点 脱离文档流 脱离文档流
应用场景 相对父元素的定位元素 固定导航、广告、弹窗
.example {
  /* 共同特性 */
  position: absolute | fixed;
  top: 0;
  left: 0;
  z-index: 10;
}

6. CSS颜色表示法

.color-examples {
  color: red;                   /* 关键词 */
  color: #FF0000;               /* 十六进制 */
  color: #F00;                  /* 简写十六进制 */
  color: rgb(255, 0, 0);        /* RGB */
  color: rgba(255, 0, 0, 0.5);  /* RGBA带透明度 */
  color: hsl(0, 100%, 50%);     /* HSL - 色相/饱和度/亮度 */
  color: hsla(0, 100%, 50%, 0.5); /* HSLA带透明度 */
  color: oklch(70% 0.2 0);      /* 新兴的色彩空间 */
}

7. CSS绘制红色爱心

image.png

   /* 爱心容器 */
      .heart {
        position: relative;
        width: 100px;
        height: 100px;
        margin: 50px auto;
      }

      /* 使用伪元素创建两个半圆 */
      .heart::before,
      .heart::after {
        content: '';
        position: absolute;
        width: 50px; /* 调整为宽度的一半 */
        height: 80px; /* 高度略大于宽度 */
        background: #ff6b81; /* 粉色系 */
        border-radius: 50px 50px 0 0; /* 只圆化左上和右上 */
      }

      /* 左半部分 */
      .heart::before {
        left: 0;
        transform: rotate(-45deg); /* 向左旋转45度 */
        transform-origin: 100% 100%; /* 以右下角为旋转中心 */
      }

      /* 右半部分 */
      .heart::after {
        left: 50%;
        transform: rotate(45deg); /* 向右旋转45度 */
        transform-origin: 0 100%; /* 以左下角为旋转中心 */
      }

8. 百分比高度参考

不是所有情况都基于容器高度:

  • height:基于容器高度计算
  • padding-top/bottom:基于容器宽度计算
  • margin-top/bottom:基于容器宽度计算
  • transform: translateY():基于元素自身高度计算
.container {
  height: 400px;
  width: 600px;
}

.child {
  height: 50%; /* 200px (400px的50%) */
  padding-top: 10%; /* 60px (600px的10%) */
  margin-bottom: 5%; /* 30px (600px的5%) */
  transform: translateY(50%); /* 自身高度的50% */
}

三、视觉优化与兼容性

9. 消除transition闪屏

/* 解决方案 */
.element {
  transform: translateZ(0);  /* 开启GPU加速 */
  backface-visibility: hidden; /* 隐藏背面 */
  perspective: 1000px;       /* 3D透视 */
  
  /* 修复iOS闪屏 */
  -webkit-transform-style: preserve-3d;
}

10. 图片格式与应用场景

格式 特性 适用场景 WebP支持
JPG 有损压缩、文件较小 照片、渐变色图像 ✓ (需转换)
PNG 无损压缩、支持透明通道 Logo、透明背景图像 ✓ (需转换)
GIF 支持动画、256色限制 简单动画、低色彩图像
WebP 更优压缩、支持透明/动画 现代浏览器中的通用图像格式 原生支持
<!-- WebP回退方案 -->
<picture>
  <source srcset="image.webp" type="image/webp">
  <img src="image.jpg" alt="备用图像">
</picture>

11. box-sizing: border-box详解

* {
  box-sizing: border-box; /* 强烈推荐的全局设置 */
}

/* box-sizing对比 */
.content-box {
  box-sizing: content-box; /* 默认值: width=内容宽度 */
  width: 200px;
  padding: 20px; /* 总宽=240px */
}

.border-box {
  box-sizing: border-box; /* width=内容+padding+border */
  width: 200px;
  padding: 20px; /* 总宽保持200px */
}

四、布局系统实践

12. 常用栅格系统对比

系统 特点 应用场景 兼容性
Bootstrap 12列布局、响应式断点 企业级应用、快速开发 优秀(IE10+)
CSS Grid 二维布局、强大对齐控制 复杂布局、现代网站 IE11部分支持
Flexbox 一维布局、弹性项目 组件内部布局 IE10部分支持
自定义栅格 灵活、按需定制 特定设计需求的网站 可控

13. 响应式设计原理与实践

/* 媒体查询示例 */
@media (max-width: 768px) {
  .container {
    grid-template-columns: 1fr;
  }
}

/* 移动优先策略 */
.component {
  padding: 1rem; /* 基础样式 */
  @media (min-width: 992px) {
    padding: 2rem; /* 大屏样式 */
  }
}

/* IE兼容方案 */
<!--[if lt IE 9]>
  <script src="html5shiv.js"></script>
  <script src="respond.min.js"></script>
<![endif]-->

14. 选中文本样式定制

::selection {
  background-color: #fd79a8; /* 粉红背景 */
  color: #fff;              /* 白色文字 */
  text-shadow: 1px 1px 2px rgba(0,0,0,.2);
}

/* 兼容旧浏览器 */
::-moz-selection {
  background: #fd79a8;
  color: #fff;
}

五、实用CSS技巧

15. 文本换行处理

/* 强制换行 */
.break-all {
  word-break: break-all;    /* 任意位置截断 */
  overflow-wrap: break-word; /* 保留单词完整 */
}

/* 中文换行 */
.chinese-text {
  word-break: break-word;
}

/* 禁止换行 */
.no-wrap {
  white-space: nowrap; 
  overflow: hidden;
  text-overflow: ellipsis; /* 超出显示省略号 */
}

16. CSS规则结构详解

/* CSS语法结构 */
selector {              /* 选择器 */
  property: value;      /* 属性: 值; */
  font-size: 16px;      /* 声明 */
}

/* 组合选择器 */
nav a:hover,            /* 伪类 */
footer > p:first-child { /* 子元素 */
  color: #3498db;
}

/* @规则 */
@import url('reset.css');
@media (min-width: 768px) {...}
@keyframes pulse {...}

17. 自适应高度布局

<div class="container">
  <div class="fixed">固定高度 (300px)</div>
  <div class="fluid">自适应剩余空间</div>
</div>
.container {
  height: 100vh;
  display: flex;
  flex-direction: column;
}

.fixed {
  height: 300px;       /* 固定高度 */
  background: #74b9ff;
}

.fluid {
  flex: 1;             /* 填充剩余空间 */
  background: #a29bfe;
  min-height: 0;       /* 防止内容溢出 */
}

18. overflow属性详解

.element {
  overflow: visible;   /* 默认值:内容溢出容器 */
  overflow: hidden;    /* 裁剪溢出内容 */
  overflow: scroll;    /* 始终显示滚动条 */
  overflow: auto;      /* 仅在需要时显示滚动条 */
  
  /* 单独设置方向 */
  overflow-x: hidden;
  overflow-y: auto;
  
  /* 滚动条美化 */
  scrollbar-width: thin; /* Firefox */
  scrollbar-color: #6c5ce7 #dfe6e9;
}

/* Chrome滚动条 */
::-webkit-scrollbar {
  width: 8px;
}

六、浏览器兼容与高级技巧

19. IE常见BUG解决方案

/* 双外边距浮动BUG (IE6) */
.floated {
  display: inline; /* 触发layout */
}

/* PNG透明度 (IE6) */
.element {
  background-image: none;
  filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(...);
}

/* min-height (IE6-9) */
.container {
  min-height: 500px;
  height: auto !important;
  height: 500px; /* IE6-7回退 */
}

20. CSS Hack写法参考

/* IE10+ */
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {...}

/* Edge */
@supports (-ms-ime-align: auto) {...}

/* IE6-9 */
.element {
  color: red\9;           /* 所有IE */
  *color: blue;           /* IE6-7 */
  _color: green;          /* IE6 */
  color: yellow\0/IE9;    /* IE9 */
}

/* IE条件注释 */
<!--[if IE 8]>
  <link rel="stylesheet" href="ie8.css">
<![endif]-->

21. 字体样式细节

/* Italic与Oblique区别 */
.text {
  font-style: italic;   /* 使用字体的斜体版本 */
  font-style: oblique;  /* 常规字体的倾斜版本 */
}

/* 实际应用 */
@font-face {
  font-family: 'MyFont';
  src: url('myfont-italic.woff') format('woff');
  font-style: italic; /* 指定为斜体专用字体 */
}

22. 自适应屏幕高度方案

html, body {
  height: 100%; /* 关键设置 */
}

/* 现代方案 */
body {
  min-height: 100vh; /* 视口高度 */
  min-height: 100dvh; /* 动态视口高度 (移动端优化) */
  
  /* 防止滚动条跳动 */
  overflow-y: scroll; 
}

/* 原因解释:
   1. 百分比高度依赖父元素明确的高度定义
   2. 视口单位(vh)直接参考设备屏幕
   3. 需要html/body的高度链式传递
*/

23. display属性全集

/* 常用display值 */
.block { display: block; } /* 块级元素 */
.inline { display: inline; } /* 行内元素 */
.inline-block { display: inline-block; } /* 行内块元素 */
.flex { display: flex; } /* 弹性布局 */
.grid { display: grid; } /* 网格布局 */
.none { display: none; } /* 完全隐藏 */

/* 特殊值 */
table { display: table; } /* 表格布局 */
.list-item { display: list-item; } /* 列表项 */
.contents { display: contents; } /* 内容布局 */

24. CSS初始化方案

/* 推荐重置样式 */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html {
  line-height: 1.5;
  -webkit-text-size-adjust: 100%; /* 移动端调整 */
}

body {
  min-height: 100vh;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
}

img {
  max-width: 100%;
  height: auto;
  display: block;
}

a {
  text-decoration: none;
  color: inherit;
}

25. BEM规范实践

/* Block-Element-Modifier */
.header { /* 块 */ }

.header__logo { /* 元素 - 属于header */ }
.header__nav { /* 元素 */ }

.header--fixed { /* 修饰符 - 固定状态 */ }
.header__nav-item--active { /* 元素修饰符 */ }

/* 其他常见规范 */
/* OOCSS 分离容器与内容 */
.container {}
.article-title {}

/* SMACSS 分类管理 */
.layout-header {}
.module-alert {}

26. 扇形绘制技巧

image.png

/* 方法1:border实现 */
.sector {
  width: 0;
  height: 0;
  border: 100px solid transparent;
  border-top: 100px solid #ff6b6b;
  border-radius: 50%;
}

/* 方法2:clip-path */
.sector-clip {
  width: 200px;
  height: 200px;
  background: #ff6b6b;
  clip-path: polygon(0 0, 100% 0, 50% 50%);
  border-radius: 0 100% 0 0;
}

/* 方法3:渐变 */
.sector-gradient {
  width: 200px;
  height: 200px;
  background: conic-gradient(#ff6b6b 0 120deg, transparent 0);
  border-radius: 50%;
}

27. 消除inline-block间隙

<!-- 消除间隙的HTML写法 -->
<div class="container">
  <span>Item1</span
  ><span>Item2</span
  ><span>Item3</span>
</div>
/* CSS解决方案 */
.container {
  font-size: 0; /* 父元素字号清零 */
}

.container > * {
  display: inline-block;
  font-size: 16px; /* 重置子元素字号 */
  vertical-align: top; /* 对齐方式统一 */
}

/* 负边距修复 */
.container {
  margin-right: -4px;
}

28. 平滑滚动优化

/* 全局平滑滚动 */
html {
  scroll-behavior: smooth; /* 基础方案 */
}

/* 容器级平滑滚动 */
.scroll-container {
  scroll-behavior: smooth;
  -webkit-overflow-scrolling: touch; /* iOS优化 */
  
  /* 性能优化 */
  will-change: scroll-position;
  overscroll-behavior: contain; /* 避免滚动链 */
}

/* JS实现平滑滚动 */
button.addEventListener('click', () => {
  element.scrollTo({
    top: 0,
    behavior: 'smooth'
  });
});

29. !important使用指南

/* 适用场景 */
.override {
  color: red !important; /* 1. 覆盖内联样式 */
}

.print-styles {
  display: none !important; /* 2. 打印样式强制 */
}

.framework-fix {
  width: calc(100% - 20px) !important; /* 3. 覆盖框架样式 */
}

/* 应避免的情况 */
.header {
  padding: 10px !important; /* 常规样式中不要使用 */
}

30. 浮动机制与清除

/* 浮动原理 */
.float-box {
  float: left; /* 脱离文档流 */
  width: 200px;
}

/* 清除浮动方法 */
/* 方法1:clearfix */
.clearfix::after {
  content: "";
  display: block;
  clear: both;
}

/* 方法2:父容器创建BFC */
.container {
  overflow: auto;
  display: flow-root; /* 推荐方式 */
}

/* 方法3:插入空元素 */
<br style="clear: both">

React 协调器 render 阶段

作者 june18
2025年7月6日 17:39

先来看个全局变量 executionContext

executionContext

executionContext 标记当前执行环境,比如当前是 render 阶段或者 commit 阶段。

类型是 number,取值有 4 种:

type ExecutionContext = number;

export const NoContext = /*             */ 0b000;
const BatchedContext = /*     批量/同步更新          */ 0b001;
export const RenderContext = /*    render 阶段     */ 0b010;
export const CommitContext = /*   commit 阶段      */ 0b100;

初始值为 NoContext

// 参考 react/packages/react-reconciler/src/ReactFiberWorkLoop.js
let executionContext: ExecutionContext = NoContext; 

两个阶段

和 render 阶段平行的还有个 commit 阶段。

  1. render 阶段,构建 fiber 树 VDOM
  2. commit 阶段,将 VDOM 转换成 DOM

两者入口如下:

// 这是每个并发任务的入口点,即通过 Scheduler 的所有内容
function performConcurrentWorkOnRoot(
  root: FiberRoot,
  didTimeout: boolean,
): RenderTaskFn | null {
  let lanes = getNextLanes(
    root,
    root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
  );
  // 没有更新
  if (lanes === NoLanes) {
    // Defensive coding. This is never expected to happen.
    return null;
  }

  // 在某些情况下,我们会禁用时间切片:如果 work 过长时间做计算(为了防止饥饿而将其视为“过期”的 work),或者我们处于默认启用同步更新模式。
  const shouldTimeSlice =
    !includesBlockingLane(root, lanes) &&
    !includesExpiredLane(root, lanes) &&
    (disableSchedulerTimeoutInWorkLoop || !didTimeout);

  let exitStatus = shouldTimeSlice
    ? renderRootConcurrent(root, lanes) // 使用时间切片
    // 1. render, 构建fiber树VDOM(beginWork|completeWork)
    : renderRootSync(root, lanes); // 不用时间切片

  // 2. commit, VDOM->DOM
  commitRoot(root);
}

renderRootSync

renderRootSync 是 render 阶段入口,包含 4 个步骤:

  1. render 阶段开始
  2. workInProgressTransitions 赋值
  3. 初始化
  4. 遍历构建 fiber 树

详细代码如下:

function renderRootSync(root: FiberRoot) {
  // 1. render阶段开始
  const prevExecutionContext = executionContext;
  executionContext |= RenderContext; // 注意这里不能用等号,如果使用等号,会覆盖其他 execution,比如如果是批量更新,如果直接覆盖,批量更新就没有了
  // 2. workInProgressTransitions 赋值
  workInProgressTransitions = getTransitionsForLanes(root, lanes);
  // 3. 初始化
  prepareFreshStack(root, lanes);

  // 4. 遍历构建 fiber 树
  workLoopSync();

  // 重置 Context 的相关值,入 currentlyRenderingFiber 等
  resetContextDependencies();

  // 4. render结束,重置 executionContext
  executionContext = prevExecutionContext;
  
  workInProgressRoot = null; // 当前正在工作的 root
  workInProgressRootRenderLanes = NoLanes;

  // 防止有新的更新,再遍历一边更新队列
  finishQueueingConcurrentUpdates();

  return workInProgressRootExitStatus;
}

function prepareFreshStack(root: FiberRoot): Fiber {
  root.finishedWork = null;

  workInProgressRoot = root; // FiberRoot
  const rootWorkInProgress = createWorkInProgress(root.current, null); // Fiber
  if (workInProgress === null) {
    workInProgress = rootWorkInProgress; // Fiber
  }

  // 把 concurrentQueues 的内容添加到 fiber 的 queue 中,即给 fiber 的 lanes、childLanes 赋值
  finishQueueingConcurrentUpdates();

  return rootWorkInProgress;
}

function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

function performUnitOfWork(unitOfWork: Fiber) {
  const current = unitOfWork.alternate;
  // 1. beginWork
  // 更新当前 fiber,比如 props/state 更新,生命周期函数执行、Hooks 函数执行等。
  // 返回下一个 fiber。
  let next = beginWork(current, unitOfWork);
  //  把 pendingProps 更新到 memoizedProps
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  // 1.1 执行自己
  // 1.2 (协调,bailout)返回子节点

  if (next === null) {
    // 没有产生新的work
    // 如果不再产生新的 work,那么当前 work 结束
    // 2. completeWork,比如生成原生节点,放在 fiber 的 stateNode 属性上
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }

  // 当前 Fiber 为 null
  ReactCurrentOwner.current = null
}

// 深度优先遍历,子节点、兄弟节点、叔叔节点、爷爷的兄弟节点...(王朝的故事)
function completeUnitOfWork(unitOfWork: Fiber) {
  let completedWork = unitOfWork;

  do {
    const current = completedWork.alternate;
    const returnFiber = completedWork.return;
    let next = completeWork(current, completedWork);
    if (next !== null) {
      workInProgress = next;
      return;
    }

    const siblingFiber = completedWork.sibling;
    if (siblingFiber !== null) {
      workInProgress = siblingFiber;
      return;
    }

    completedWork = returnFiber as Fiber;
    workInProgress = completedWork;
  } while (completedWork !== null);
}

sibling-index:我用这个画时钟表盘

作者 柚子816
2025年7月6日 17:19

如果你需要画一个时钟表盘,你会怎么做?用12个div元素,使用CSS的transform属性来旋转它们,来将他们放在正确的位置。但是,你会发现,你需要使用JavaScript来计算每个div元素的旋转角度, 当前你也可以提前计算好,但终究是要计算的。一但钟表半径变化,你就需要重新计算。

那如何我们现在添加一些限制条件呢

<div class="panel">
  <div class="digit"></div>
  <div class="digit"></div>
  <div class="digit"></div>
  <div class="digit"></div>
  <div class="digit"></div>
  <div class="digit"></div>
  <div class="digit"></div>
  <div class="digit"></div>
  <div class="digit"></div>
  <div class="digit"></div>
  <div class="digit"></div>
  <div class="digit"></div>
</div>

使用上面的html片段,不使用nth-*之类的选择器,不使用+选择器。纯CSS应该如何实现一个表盘呢

先添加一些基础样式

.panel {
  width: 200px;
  height: 200px;
  border-radius: 50%;
  border: 3px solid #000;
  position: relative;
}
.digit {
  position: absolute;
  left: 90px;
  top: 90px;
  width: 20px;
  height: 20px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 14px;
}

填充12个刻度

再没有JS的情况下,我们可以使用::after伪元素来实现向页面添加显示内容。那1-12这些数字应该实现如何呢。在html中有什么情况会自动为元素填充序号呢?没错,有序列表。那我们这里其实也类似,在CSS中有counter功能

.panel {
  counter-reset: digit;
}
.digit {
  counter-increment: digit;

  &::after {
    content: counter(digit);
  }
}

这样每个div中都会显示一个数字。接下来,我们需要将这些数字旋转到正确的位置。我们可以使用transform属性来实现。问题是,我们如何知道每个数字的旋转角度呢?我们可以使用sibling-index函数来实现。这个函数可以返回一个元素在兄弟元素当中的索引。我们可以使用这个函数来计算每个数字的旋转角度。

.digit {
  --angle: calc((sibling-index() - 3) * 30deg);
  transform: translate(calc(cos(var(--angle)) * 90px), calc(sin(var(--angle)) * 90px));
}

附上完整代码

<style>
  .panel {
    width: 200px;
    height: 200px;
    border-radius: 50%;
    border: 3px solid #000;
    position: relative;

    counter-reset: digit;
  }

  .digit {
    position: absolute;
    left: 90px;
    top: 90px;
    width: 20px;
    height: 20px;
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 14px;

    counter-increment: digit;

    --angle: calc((sibling-index() - 3) * 30deg);
    transform: translate(calc(cos(var(--angle)) * 90px), calc(sin(var(--angle)) * 90px));

    &::after {
      content: counter(digit);
    }
  }
</style>

<div class="panel">
  <div class="digit"></div>
  <div class="digit"></div>
  <div class="digit"></div>
  <div class="digit"></div>
  <div class="digit"></div>
  <div class="digit"></div>
  <div class="digit"></div>
  <div class="digit"></div>
  <div class="digit"></div>
  <div class="digit"></div>
  <div class="digit"></div>
  <div class="digit"></div>
</div>

效果如下

clock.png

兼容性

目前只有chrome最新版本支持这个函数,其他浏览器还没有支持。谨慎使用

JavaScript :字符串模板——优雅编程的基石

作者 轻语呢喃
2025年7月6日 17:18

在 JavaScript 开发中,字符串拼接曾是开发者最频繁的操作之一。

随着 ES6 的普及,字符串模板(Template String)作为一项革命性特性,彻底改变了我们处理字符串的方式,它不仅简化了代码逻辑,而且提升了可读性和可维护性。

本文将结合数组方法、Symbol 类型等内容,深入解析字符串模板的核心原理和实际应用。


一、字符串模板:优雅的字符串操作

1. 基础语法与核心优势

字符串模板通过反引号(`)定义,支持多行字符串和嵌入变量,语法如下:

const name = "狗·德川家康·薛定谔·保留";
const age = 3;
const sentence = `我的猫 ${name} 今年 ${age * 5} 岁.`;
console.log(sentence);
// 输出:我的猫 狗·德川家康·薛定谔·保留 今年 15 岁.

核心优势:

  • 多行字符串支持:无需 \n 或字符串拼接即可实现多行文本。
  • 变量嵌入:通过 ${expression} 语法直接嵌入表达式,告别繁琐的 + 拼接。
  • 代码可读性:逻辑清晰,减少冗余字符(如引号闭合、转义符号)。

对比传统方式:

// 传统拼接
const sentenceOld = "我的猫 " + name + " 今年 " + (age * 5) + " 岁.";
// 字符串模板
const sentenceNew = `我的猫 ${name} 今年 ${age * 5} 岁.`;

2. 多行字符串的实际应用

字符串模板对多行字符串的处理尤其强大,尤其适合生成 HTML 结构或格式化文本:

const html = `
  <div class="user-card">
    <h2>${user.name}</h2>
    <p>年龄:${user.age}岁</p>
    <p>地址:${user.address}</p>
  </div>
`;

与 DOM 操作结合:

 const friends = [
            { name: '刘备', age: 520, address: '桃园' },
            { name: '关羽', age: 114, address: '同上' },
            { name: '张飞', age: 514, address: '俺也一样' }
        ];
const ul = document.getElementById('friends');
ul.innerHTML = friends.map(friend => `
  <li>
    ${friend.name} , <i>${friend.age}岁</i> , <b>${friend.address}</b>
  </li>
`).join('');

image.png

关键点解析:

  • map 方法:遍历数组并返回新数组,每个元素为 <li> 的 HTML 字符串。
  • .join(''):将数组转换为字符串,避免 innerHTML 中出现逗号分隔符。
  • 字符串模板优势:无需手动拼接 <li> 标签,结构清晰,格式自然保留。

二、数组方法与字符串模板的协同:map 的深度解析

1. map 方法的核心特性

map 是 ES6 为数组提供的高阶函数,用于将数组中的每个元素映射为新形式,返回新数组:

const numbers = [1, 2, 3];
const squares = numbers.map(n => n * n);
console.log(squares); // [1, 4, 9]

与字符串模板结合的典型场景:

  • 数据格式转换:将 JSON 数据转化为 HTML 元素。
  • 动态渲染:根据数据生成动态内容(如表格、列表等)。

代码案例:

const users = [
  { id: 1, name: 'Alice', role: 'Admin' },
  { id: 2, name: 'Bob', role: 'User' }
];

const tableBody = users.map(user => `
  <tr>
    <td>${user.id}</td>
    <td>${user.name}</td>
    <td>${user.role}</td>
  </tr>
`).join('');

document.getElementById('user-table').innerHTML = tableBody;

2. mapforEach 的对比

特性 map forEach
返回值 新数组(映射后的结果) undefined
适用场景 数据转换、生成新数组 仅需遍历,无需返回新数组
示例 numbers.map(n => n * 2) arr.forEach(item => console.log(item))

三、Symbol 类型:对象属性的唯一性保障

1. Symbol 的基本概念

Symbol 是 ES6 引入的原始数据类型,表示独一无二的值,常用于定义对象的私有属性:

const sym1 = Symbol();
const sym2 = Symbol('desc'); // 可选描述信息
console.log(sym1 === sym2); // false

作为对象键的使用:

const ID = Symbol('id');
const user = {
  name: 'Alice',
  [ID]: 123,
  [Symbol('age')]: 20
};

console.log(user[ID]); // 123
console.log(user[Symbol('age')]); // undefined(Symbol 是唯一的)

关键优势:

  • 避免键名冲突:Symbol 作为键时,不会与字符串键冲突。
  • 私有属性模拟:Symbol 键无法通过 for...in 遍历,适合模拟私有属性。

代码案例:

const user = {
  name: 'Alice',
  [Symbol('id')]: 123,
  age: 18
};

for (let key in user) {
  console.log(key); // 输出 name 和 age,不会包含 Symbol 键
}

2. Symbol 与字符串键的区别

特性 字符串键(String Key) Symbol 键(Symbol Key)
唯一性 可能冲突(如 age vs 'age' 始终唯一
遍历性 可遍历(for...in 默认不可遍历(需 Object.getOwnPropertySymbols()
适用场景 公共属性 私有属性、元数据

四、枚举类型:用 Symbol 实现状态管理

1. 枚举的实现原理

JavaScript 本身没有原生的枚举类型,但可以通过对象或 Symbol 模拟:

const status = {
  ready: Symbol('ready'),
  running: Symbol('running'),
  done: Symbol('done')
};

let state = status.ready;

if (state === status.ready) {
  console.log('准备就绪');
}

优势:

  • 常量不可变:Symbol 值不会被意外修改。
  • 避免命名冲突:Symbol 是唯一的,不会与其他键冲突。

五、实际应用场景:从数据到 UI 的完整流程

1. 动态渲染用户列表

结合字符串模板、map 和 DOM 操作,实现动态渲染:

<ul id="user-list"></ul>
<script>
  const friends = [
            { name: '刘备', age: 520, address: '桃园' },
            { name: '关羽', age: 114, address: '同上' },
            { name: '张飞', age: 514, address: '俺也一样' }
        ];

  const userList = document.getElementById('user-list');
  userList.innerHTML = users.map(user => `
    <li>
      <strong>${user.name}</strong>(<em>${user.age}岁</em>)<br>
      <small>地址:${user.address}</small>
    </li>
  `).join('');
</script>

2. 数据格式转换:JSON 到 HTML 表格

将 JSON 数据转换为 HTML 表格,展示 map 与字符串模板的协同作用:

const products = [
  { id: 1, name: '苹果', price: 5 },
  { id: 2, name: '香蕉', price: 3 }
];

const table = `
  <table border="1">
    <thead>
      <tr><th>ID</th><th>名称</th><th>价格</th></tr>
    </thead>
    <tbody>
      ${products.map(p => `
        <tr>
          <td>${p.id}</td>
          <td>${p.name}</td>
          <td>¥${p.price}</td>
        </tr>
      `).join('')}
    </tbody>
  </table>
`;
document.body.innerHTML = table;

六、总结

1. 核心知识点回顾

  • 字符串模板:通过反引号和 ${} 实现多行字符串和变量嵌入。
  • map 方法:用于数据转换,与字符串模板结合可高效生成动态内容。
  • Symbol 类型:提供唯一键,适合定义私有属性和避免命名冲突。
  • 枚举模拟:通过 Symbol 实现状态管理,提升代码健壮性。

2. 实战建议

  • 优先使用字符串模板:替代传统的字符串拼接,提升代码可读性。
  • 合理使用 map:在需要返回新数组的场景中,优先选择 map 而非 forEach
  • Symbol 用于敏感属性:对需要保护的属性(如私有 ID),使用 Symbol 键。
  • 避免过度嵌套:字符串模板嵌套层级过深时,可拆分为多个变量或函数。

3. 常见误区与解决方案

  • 问题 1map 返回的数组未用 .join('') 转换,导致 innerHTML 中出现逗号。
    • 解决方案:始终用 .join('') 处理数组。
  • 问题 2:Symbol 键被意外暴露。
    • 解决方案:通过 Object.getOwnPropertySymbols() 访问 Symbol 键。

50行代码搞定OneCode摄像头插件:快速定制实战指南

作者 Codebee
2025年7月6日 17:12

引言

在工业监控、人脸识别等场景中,摄像头集成是常见需求。本文将以OneCode平台的xui.UI.Camera组件为例,展示如何用50行核心代码实现一个功能完备的摄像头插件,涵盖设备访问、视频流显示和拍照功能,并提炼OneCode插件开发的核心要素。

一、核心代码实现(50行精简版)

xui.Class("xui.UI.Camera", "xui.UI", {
    Instance: {
        // 初始化摄像头
        initCamera: function() {
            const video = this.getSubNode("H5").dom;
            navigator.mediaDevices.getUserMedia({ video: true })
                .then(stream => video.srcObject = stream)
                .catch(e => xui.log("摄像头访问失败:", e));
        },
        // 拍照功能
        captureImage: function() {
            const canvas = document.createElement("canvas");
            const video = this.getSubNode("H5").dom;
            canvas.width = video.videoWidth;
            canvas.height = video.videoHeight;
            canvas.getContext("2d").drawImage(video, 0, 0);
            return canvas.toDataURL("image/png");
        }
    },
    Static: {
        Templates: {
            tagName: 'div',
            H5: { tagName: 'video', autoplay: true, playsinline: true },
            COVER: { tagName: 'div', style: "background-image:url("+xui.ini.img_bg+");" }
        },
        DataModel: {
            width: { $spaceunit:1, ini:'34em' },
            height: { $spaceunit:1, ini:'25em' },
            captureBtn: { ini: true, action: function(v) { v && this.boxing().initCamera(); } }
        },
        RenderTrigger: function() { this.boxing().initCamera(); }
    }
});

二、技术点深度解析

2.1 媒体设备访问机制

核心依赖Web API getUserMedia实现摄像头数据流捕获:

navigator.mediaDevices.getUserMedia({ video: true })
    .then(stream => video.srcObject = stream)
  • 权限处理:自动触发浏览器摄像头权限请求
  • 流处理:直接将MediaStream对象赋值给video元素的srcObject
  • 错误处理:捕获设备访问失败场景(无摄像头/权限拒绝)

2.2 OneCode组件三要素

  1. 模板系统(Templates)
Templates: {
    tagName: 'div',                // 根容器
    H5: { tagName: 'video', ... }, // 视频播放元素
    COVER: { ... }                 // 覆盖层
}

采用声明式DOM结构,支持动态属性绑定(如autoplay: {_autoplay}

  1. 数据模型(DataModel)
DataModel: {
    width: { $spaceunit:1, ini:'34em' },  // 支持响应式单位
    captureBtn: {                         // 功能开关属性
        ini: true,
        action: function(v) { ... }       // 属性变化回调
    }
}
  • $spaceunit: 启用单位转换(em/px自动适配)
  • action: 属性值变化时的触发逻辑
  1. 渲染触发器(RenderTrigger)
RenderTrigger: function() {
    this.boxing().initCamera();  // 组件渲染完成后初始化摄像头
}

组件生命周期关键节点,确保DOM就绪后执行初始化逻辑

2.3 拍照功能实现

利用Canvas API实现视频帧捕获:

captureImage: function() {
    const canvas = document.createElement("canvas");
    // 匹配视频分辨率
    canvas.width = video.videoWidth;
    canvas.height = video.videoHeight;
    // 绘制当前视频帧
    canvas.getContext("2d").drawImage(video, 0, 0);
    return canvas.toDataURL("image/png"); // 返回base64图片
}
  • 分辨率适配:使用video实际分辨率确保图像清晰
  • 数据格式:支持base64编码,便于后续上传或显示

三、OneCode插件开发核心要素

3.1 类继承体系

xui.Class("xui.UI.Camera", "xui.UI", { ... })
  • 继承xui.UI基类获取组件基础能力
  • 复杂场景可继承特定组件(如xui.UI.Audio

3.2 实例方法设计

  • 初始化方法initCamera() - 处理设备访问
  • 功能方法captureImage() - 实现核心业务逻辑
  • 事件处理:可扩展onCaptureSuccess等回调方法

3.3 属性驱动开发

通过DataModel定义可配置属性,实现组件灵活性:

// 支持动态调整分辨率
resolution: {
    ini: "720p",
    listbox: ["480p", "720p", "1080p"],
    action: function(v) {
        const constraints = { video: { width: { ideal: v=="1080p"?1920:v=="720p"?1280:640 } } };
        // 重新初始化摄像头
    }
}

3.4 跨浏览器兼容性

  • 前缀处理:针对旧浏览器提供兼容性封装
const getUserMedia = navigator.mediaDevices.getUserMedia ||
    navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
  • 特性检测:提前检查浏览器支持情况
if (!getUserMedia) {
    this.getSubNode("H5").html("您的浏览器不支持摄像头功能");
}

四、快速扩展指南

4.1 添加拍照按钮

// 在Templates中添加按钮
CAPTURE_BTN: { tagName: 'button', text: '拍照', onclick: 'capture' }
// 添加点击事件处理
Instance: { capture: function() { const img = this.captureImage(); ... } }

4.2 实现图片上传

uploadImage: function(base64Data) {
    xui.ajax({ url: '/upload', method: 'POST', data: { image: base64Data } });
}

4.3 视频录制功能

基于MediaRecorder API扩展:

startRecord: function() {
    this.recorder = new MediaRecorder(this.stream);
    this.chunks = [];
    this.recorder.ondataavailable = e => this.chunks.push(e.data);
    this.recorder.start();
}

五、最佳实践

  1. 权限管理:提供明确的权限申请提示
  2. 错误处理:覆盖设备未找到、权限拒绝等场景
  3. 资源释放:组件销毁时停止视频流
destroy: function() {
    if (this.stream) this.stream.getTracks().forEach(track => track.stop());
}
  1. 性能优化:根据网络状况动态调整分辨率

结语

通过本文示例,我们展示了如何用50行核心代码实现一个基础摄像头插件。OneCode平台的组件化设计极大简化了开发流程,开发者只需关注核心业务逻辑。实际项目中,可基于此扩展人脸识别、二维码扫描等高级功能,满足不同场景需求。

提示:完整代码可参考xui.UI.Camera组件源码,更多API细节请查阅OneCode官方文档。

智能前端小魔术,让图片开口说单词

作者 归于尽
2025年7月6日 17:12

这几天做了个小项目 —— 用户上传图片后,AI能自动识别出对应的英文单词,还能生成例句和语音。整个过程踩了不少坑,也有很多值得说道的细节,今天就从头到尾捋一捋实现思路,尤其是那些容易被忽略的技术点。

从主组件开始

任何 React 项目都需要一个主组件来串联各个功能,App.jsx 就是这个项目的 “大脑”,负责管理全局状态和协调各个模块。先看最基础的结构:

import { useState } from "react";
import "./App.css";
import PictureCard from "./components/PictureCard";
import { generateAudio } from "./lib/audio";

function App() {
  // 后面会逐步添加状态和方法

  return (
    <div className='container'>
      {/* 内容后续填充 */}
    </div>
  );
}

export default App;

这个组件的核心任务有三个:存储项目中所有需要共享的数据(比如识别出的单词、语音地址)、处理 AI 接口调用逻辑、把数据和方法传递给子组件。

定义状态

首先要明确,这个项目需要存储哪些数据?我列了一下,至少包括这些:

function App() {
  // 英文单词
  const [word, setWord] = useState("请上传图片");
  // 英文例句
  const [sentence, setSentence] = useState("");
  // 语音地址
  const [audio, setAudio] = useState("");
  // 控制详情区域是否展开
  const [detailExpand, setDetailExpand] = useState(false);
  // 图片的预览地址
  const [imgPreview, setImgPreview] = useState("");
  // 单词的详细解释
  const [explanation, setExplanation] = useState([]);
  // 解释中问句的回复内容
  const [expReply, setExpReply] = useState([]);

}

这些状态各自有明确的用途:word 展示核心单词,sentence 展示例句,audio 存储语音地址…… 最关键的是,这些状态会在不同组件间流转,比如 word 和 audio 会传给子组件 PictureCard 展示,而 imgPreview 则会在详情区域显示。

搭建页面结构

状态定义好了,接下来要考虑如何把这些数据展示给用户。页面结构其实很简单,主要分为两部分:图片上传区和结果展示区。

return (
  <div className='container'>
 
    <PictureCard 
      word={word} 
      audio={audio}
      uploadImg={uploadImg}
    />

    {/* 结果展示区域 */}
    <div className="output">
      <div className="sentence">{sentence}</div>

      {/* 详情展开/收起区域 */}
       <div className="details">
          <button onClick={() => setDetailExpand(!detailExpand)}>Talk about it</button>
          {
            detailExpand ? (
              <div className="expand">
                <img src={imgPreview} alt="preview"/>
                {
                  explanation.map((explanation, index) => (
                    <div key={index} className="explanation">
                      {explanation}
                    </div>
                  ))
                }
                {
                  expReply.map((reply, index) => (
                    <div key={index} className="reply">
                      {reply}
                    </div>
                  ))
                }
              </div>
            ): (
              <div className="fold" />
            )
          }
        </div>
    </div>
  </div>
);

这段代码的逻辑很清晰:

  • 用 PictureCard 组件处理图片上传,把需要展示的数据(wordaudio)和处理方法(uploadImg)传进去
  • 结果展示区分为两部分:直接显示的例句,和可展开的详情(包含图片、解释、回复)
  • 点击按钮时通过 setDetailExpand 切换详情的显示状态,这是 React 中常见的条件渲染方式

图片上传与预览

PictureCard 组件是用户交互的入口,负责接收用户上传的图片并实时预览,同时把图片数据传给 App 组件处理。先看它的基础结构:

import { useState } from "react";
import "./style.css";

const PictureCard = (props) => {
  const { word, audio, uploadImg } = props;

  const [imgPreview, setImgPreview] = useState(
    "https://res.bearbobo.com/resource/upload/W44yyxvl/upload-ih56twxirei.png"
  );


  return (
    <div className="card">
      {/* 上传相关内容 */}
    </div>
  );
};

export default PictureCard;

这个组件的核心任务是:让用户能选择图片 → 读取图片内容 → 实时预览 → 把图片数据传给父组件。

实现图片选择与预览功能

return (
  <div className="card">
    <input
      type="file"
      id="selectImage"
      accept=".jpg,.jpeg,.png,.gif" 
      onChange={uploadImgData}
    />
    
    {/* label 与 input 关联,点击图片就会打开文件选择框 */}
    <label htmlFor="selectImage" className="upload">
      <img src={imgPreview} alt="preview" />
    </label>
  </div>
);

选择图片需要一个文件输入框,但原生的输入框样式不好控制,所以用了个小技巧:把 input[type="file"] 隐藏起来,用 label 关联它,这样就能自定义上传按钮的样式。点击 label 里的图片时,其实触发的是文件选择框,体验会好很多。 接下来是关键的 uploadImgData 方法,它负责读取用户选择的图片并转换格式:

const uploadImgData = async (e) => {
  // 获取用户选择的第一个文件
  const file = e.target.files?.[0];
  
  if (!file) return;

  // 用 Promise 包装,方便父组件知道图片处理完成的时机
  return new Promise((resolve, reject) => {
    // 创建 FileReader 实例,用于读取文件内容
    const reader = new FileReader();
    
    // 以 DataURL 格式读取文件(会把文件转成 base64 字符串)
    reader.readAsDataURL(file);

   
    reader.onload = () => { 
      const data = reader.result;
      
      setImgPreview(data);
      
      uploadImg(data);
      
      resolve(data);
    };

    // 读取失败的回调
    reader.onerror = (error) => {
      reject(error);
    };
  });
};

最关键的是 uploadImgData 函数,这里用到了 FileReader API。刚开始我想直接把文件对象传给后端,后来发现前端预览必须先读文件内容。readAsDataURL 方法会把文件转换成 base64 格式的字符串,长得像 data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA... 这种。

为什么要用 base64?因为它可以直接作为 img 标签的 src 属性值,不用上传到服务器就能预览。当 reader.onload 触发时,reader.result 就是转换好的 base64 字符串,把它存到 imgPreview 状态里,图片就实时显示出来了。

这里踩过一个坑:如果取消选择文件,e.target.files 会是空的,所以必须加个判断 if (!file) return,否则会报错。另外,用 Promise 包装读取过程,是为了让父组件能知道图片什么时候处理完,方便后续调用 API。

调用月之暗面 API

图片数据传到 App 组件后,下一步就是调用 AI 接口分析图片内容。这部分逻辑在 uploadImg 方法里,这个方法是 PictureCard 组件传图片数据时触发的。

准备提示词

调用 AI 接口时,提示词(prompt)非常关键,直接决定返回结果的质量。我专门写了一段提示词,明确告诉 AI 需要做什么、返回什么格式的数据:

// 定义提示词,指导 AI 如何分析图片
const picPrompt = `
  请分析图片内容,找出最能描述图片的一个英文单词,尽量选择 A1-A2 难度的基础词汇。
  必须严格按照以下 JSON 格式返回结果,不要添加任何额外内容:
  { 
    "image_description": "对图片内容的简要描述",
    "representative_word": "最能代表图片的英文单词",
    "example_sentence": "包含该单词的简单英文例句",
    "explanation": "用英文解释这个单词,要求每句单独一行,以 Look at... 开头,最后加一个与日常生活相关的问句",
    "explanation_reply": ["对问句的第一个回复(英文)", "对问句的第二个回复(英文)"]
  }
`;

提示词里强调了几点:单词难度(A1-A2)、严格的 JSON 格式、解释的结构(每句一行 + 问句)。刚开始没限制格式时,AI 经常返回大段文本,解析起来很麻烦,加了格式限制后就顺畅多了。

实现 API 调用逻辑

有了提示词,就可以编写调用接口的代码了:

const uploadImg = async (imageData) => {
  setImgPreview(imageData);
  setWord("分析中...");

  // 月之暗面 API 的地址
  const endpoint = "https://api.moonshot.cn/v1/chat/completions";
  // 请求头
  const headers = {
    "Content-Type": "application/json",
    // 从环境变量获取 API 密钥,避免明文暴露
    Authorization: `Bearer ${import.meta.env.VITE_KIMI_API_KEY}`
  };
  try {
    // 发送 POST 请求
    const response = await fetch(endpoint, {
      method: "POST",
      headers,
      body: JSON.stringify({
        model: "moonshot-v1-8k-vision-preview", 
        messages: [
          {
            role: "user",
            content: [
              {
                type: "image_url",
                image_url: { url: imageData }
              },
              {
                type: "text",
                text: picPrompt
              }
            ]
          }
        ],
        stream: false 
      })
    });

    // 解析接口返回的 JSON 数据
    const data = await response.json();
    const replyData = JSON.parse(data.choices[0].message.content);

    // 更新状态,让页面显示识别结果
    setWord(replyData.representative_word); 
    setSentence(replyData.example_sentence);
   
    setExplanation(replyData.explanation.split("\n"));
    setExpReply(replyData.explanation_reply);

    // 调用 TTS 接口生成语音(后面详细讲)
    const audioUrl = await generateAudio(replyData.example_sentence);
    setAudio(audioUrl); 
  } catch (error) {
    // 出错时显示错误提示
    setWord("分析失败,请重试");
    console.error("API 调用出错:", error);
  }
};

这段代码的关键步骤:

  1. imageData 是从子组件传过来的 base64 字符串,直接作为图片 URL 传给 API,省了先上传到服务器的步骤。
  2. 请求头里的 Authorization 用了环境变量 VITE_KIMI_API_KEY,这是为了安全,密钥不能明文写在代码里,用 Vite 的环境变量管理很方便。
  3. 调用 API 时指定了 model: "moonshot-v1-8k-vision-preview",这是月之暗面支持图片识别的模型。
  4. 拿到返回结果后,先用 JSON.parse 解析,因为 AI 返回的是字符串格式的 JSON,必须转成对象才能用。
  5. 最后调用 generateAudio 生成语音,把例句读出来,这个函数的实现是另一个重点。

按理说我们拿到了数据应该去渲染页面,但是我为了偷懒直接把页面放在前面了😁😁

展示单词和语音播放按钮

除了上传功能,这个组件还要显示识别出的单词和语音播放按钮。在 return 里补充这部分内容:

return (
  <div className="card">
   
    <input type="file" id="selectImage" ... />
    <label htmlFor="selectImage" className="upload">
      <img src={imgPreview} alt="preview" />
    </label>

    <div className="word">{word}</div>

    {/* 语音播放按钮,只有当 audio 有值时才显示 */}
    {audio && (
      <div className="playAudio" onClick={playAudio}>
        <img
          width="20px"
          src="https://res.bearbobo.com/resource/upload/Omq2HFs8/playA-3iob5qyckpa.png"
          alt="play"
        />
      </div>
    )}
  </div>
);

再实现播放语音的方法:

// 播放语音的方法
const playAudio = () => {
  const audioEle = new Audio(audio);
  audioEle.play();
};

这里的逻辑很简单:当父组件传来 audio 地址时,渲染播放按钮;点击按钮时,用 Audio 构造函数创建音频实例并播放。

实现 TTS 功能

最后一步是把生成的例句转成语音,这部分逻辑放在 lib/audio.js 里,涉及到 base64 解码和 Blob 处理,是项目的技术亮点之一。

调用 TTS 接口生成音频数据

首先实现调用 TTS 服务的方法,获取音频的 base64 数据:

// lib/audio.js
export const generateAudio = async (text) => {
  // 从环境变量获取配置
  const token = import.meta.env.VITE_AUDIO_ACCESS_TOKEN;
  const appId = import.meta.env.VITE_AUDIO_APP_ID;
  const clusterId = import.meta.env.VITE_AUDIO_CLUSTER_ID;
  const voiceName = import.meta.env.VITE_AUDIO_VOICE_NAME;

  // TTS 接口地址
  const endpoint = "/tts/api/v1/tts";
  // 请求头
  const headers = {
    "Content-Type": "application/json",
    Authorization: `Bearer;${token}`
  };

  // 构造请求体,不同服务的参数格式可能不同
  const payload = {
    app: {
      appid: appId,
      token,
      cluster: clusterId
    },
    user: {
      uid: "ai-word-user" // 自定义用户 ID
    },
    audio: {
      voice_type: voiceName, // 发音人
      encoding: "mp3", // 音频格式
      rate: 24000, // 采样率
      speed_ratio: 1.0, // 语速
      emotion: "neutral" // 情感
    },
    request: {
      reqid: Math.random().toString(36).substring(2, 10), // 随机请求 ID
      text, // 要转换的文本(例句)
      text_type: "plain" // 文本类型为纯文本
    }
  };

  try {
    // 调用 TTS 接口
    const response = await fetch(endpoint, {
      method: "POST",
      headers,
      body: JSON.stringify(payload)
    });
    const data = await response.json();
    // 把返回的 base64 音频数据转成可播放的 URL
    return getAudioUrl(data.data);
  } catch (error) {
    console.error("语音生成失败:", error);
    return null;
  }
};

这个函数的作用是把例句文本传给 TTS 服务,拿到音频数据。这里的 reqid 用随机字符串生成,是为了避免请求冲突。返回的 data.data 是 base64 编码的音频内容,不能直接用,调用 getAudioUrl 方法把数据转成浏览器能播放的格式。

将 base64 音频数据转换为可播放的 URL

TTS 服务返回的是 base64 编码的音频数据,不能直接播放,需要将其转换为浏览器可以识别的格式:

const getAudioUrl = (base64Data) => {
  // 1. 解码 base64 字符串,得到二进制字符串
  const byteCharacters = atob(base64Data);
  
  // 2. 创建字节数组
  const byteNumbers = new Array(byteCharacters.length);
  for (let i = 0; i < byteCharacters.length; i++) {
    byteNumbers[i] = byteCharacters.charCodeAt(i);
  }
  
  // 3. 创建 Uint8Array(8 位无符号整数数组)
  const byteArray = new Uint8Array(byteNumbers);
  
  // 4. 创建 Blob 对象(二进制大对象)
  const blob = new Blob([byteArray], { type: 'audio/mp3' });
  
  // 5. 生成可播放的 URL
  return URL.createObjectURL(blob);
};

这个过程涉及几个关键步骤:

  1. 解码 base64:使用 atob() 函数将 base64 字符串解码为原始二进制数据

  2. 转换为字节数组:将二进制字符串转换为数字数组,每个数字对应一个字节

  3. 创建类型化数组:使用 Uint8Array 将普通数组转换为 JavaScript 可以处理的二进制数据

  4. 创建 Blob 对象:将二进制数据封装为 Blob 对象,并指定 MIME 类型为 audio/mp3

  5. 生成 URL:使用 URL.createObjectURL() 为 Blob 对象生成一个临时 URL,这个 URL 可以直接作为 <audio> 元素的 src

为什么不直接使用 base64 字符串作为音频源?虽然可以这样做,但性能会较差,尤其是对于较大的音频文件。而使用 Blob URL 可以让浏览器更高效地处理音频数据,并且在不再需要时可以通过 URL.revokeObjectURL() 释放资源。

好了,整个项目的流程就是这样了,下面让我们上传一张图片来看看具体效果吧!

image.png

这个项目虽然功能不复杂,但把图片处理、AI 接口调用、语音生成这些知识点串起来了。尤其是前端直接处理文件和二进制数据的部分,以前总觉得很深奥,实际做起来才发现,掌握 FileReaderBlobURL 这些 API 后,很多需求都能迎刃而解。最后想问一句,这个苹果是不是看起来很好吃?

项目地址:github.com/LgFvm9353/l…

vite 插件

2025年7月6日 17:12

1. vite 插件介绍

  • 服务器启动阶段: optionsbuildStart钩子会在服务启动时被调用。

  • 请求响应阶段: 当浏览器发起请求时,Vite 内部依次调用resolveIdloadtransform钩子。

  • 服务器关闭阶段: Vite 会依次执行buildEndcloseBundle钩子。

  • moduleParsed: 模块解析后触发,允许你修改模块的元数据。 在开发阶段不被调用,因为 Vite 使用的是热重载和即时模块更新,不需要完整的模块解析阶段。

  • renderChunk:生成输出块时调用,允许你自定义生成的代码块。 开发阶段不调用,因为 Vite 主要依赖浏览器的即时更新,而非生成最终的打包输出。

1.1 强制插件排序

import typescript2 from 'rollup-plugin-typescript2'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [
    {
      ...typescript2(),
      apply: 'build',
    },
    {
      ...image(),
      enforce: 'pre',
    },
  ],
})
  • pre: 在vite核心插件之前调用这个插件
  • post: 在vite核心插件之后调用这个插件
  • 默认:在 Vite 核心插件之后调用该插件

1.2 情景应用

  • 开发 (serve) 和生产 (build) 默认都会调用
  • apply 属性指明它们仅在 'build' 或 'serve' 模式时调用:

注意:名字 vite-plugin-xxx

2. 虚拟模块

  • 虚拟模块就像是你凭空创造出来的 JS 文件,不在电脑上真实存在,但可以被其他模块像普通文件一样导入使用。

适用场景 - 动态生成配置 运行时变量注入 按需生成工具函数

2.1

import type { Plugin } from 'vite'

// 定义虚拟模块ID
const virtualModuleId = 'virtual:fibonacci'
const resolvedVirtualModuleId = '\0' + virtualModuleId

export default function virtualFibPlugin(): Plugin {
  return {
    name: 'vite-plugin-virtual-fib',

    // 解析虚拟模块ID
    resolveId(id) {
      console.log(id, 'resolveId')
      if (id === virtualModuleId) {
        return resolvedVirtualModuleId
      }
    },

    // 加载虚拟模块内容
    load(id) {
      console.log(id, 'load')
      if (id === resolvedVirtualModuleId) {
        return `
          // 斐波那契数列实现
          export function fib(n) {
            return n <= 1 ? n : fib(n - 1) + fib(n - 2)
          }

          // 记忆化版本
          export function memoFib(n, memo = {}) {
            if (n in memo) return memo[n]
            if (n <= 1) return n
            memo[n] = memoFib(n - 1, memo) + memoFib(n - 2, memo)
            return memo[n]
          }
        `
      }
    }
  }
}

注意1:加 \0 公共插件使用 virtual:插件名 格式(如 virtual:posts)

注意2: 虚拟模块在生产的时候 最好显示配置一下

export default {
  plugins: [virtualPlugin()],
  build: {
    rollupOptions: {
      plugins: [virtualPlugin()] // 显式注册
    }
  }
}

🤔:不配置啥后果 虚拟模块在生产环境中的可用性取决于具体场景

  • 基本虚拟模块(仅生成静态内容):

    • 通常可以工作:如果插件只是生成简单的静态内容
    • 原因:Vite 会继承主插件数组中的插件配置
  • 复杂虚拟模块(依赖特定钩子或转换):

    • ❌ 可能失败:如果插件依赖 build 阶段的特定钩子
    • 报错表现:Cannot find module 'virtual:xxx' 或生成的内容不正确

3. vite特有钩子

  • config 在解析 Vite 配置前调用。钩子接收原始用户配置
  • configResolved 在解析 Vite 配置后调用 使用这个钩子读取和存储最终解析的配置
  • configureServer 是用于配置开发服务器的钩子
  • configurePreviewServer 用于定制预览服务器的钩子,类似configureServer 但专门用vite preview 命令启动的预览服务器。
  • transformIndexHtml 专门用来修改 index.html 文件的钩子
  • handleHotUpdate 自定义热模块替换(HMR)行为的钩子

3.1 配置处理钩子

config - 修改配置

config(config, { command, mode }) {
  // command: 'serve'开发模式 | 'build'生产模式
  // mode: 'development' | 'production' | 自定义模式

  if (command === 'serve') {
    return {
      server: {
        port: 3000, // 修改开发服务器端口
        open: true  // 自动打开浏览器
      }
    }
  }
  // 生产环境配置
  return {
    build: {
      minify: 'terser'
    }
  }
}

configResolved - 配置确认

const myPlugin = () => {
  let viteConfig // 用于存储配置
  return {
    name: 'my-plugin',
    // configResolved 钩子
    configResolved(resolvedConfig) {
      // 存储最终解析的配置
      viteConfig = resolvedConfig
      console.log('当前运行模式:', viteConfig.command)
    },

    // 在其他钩子中使用配置
    transform(code, id) {
      if (viteConfig.command === 'serve') {
        console.log('开发模式处理:', id)
      } else {
        console.log('生产构建处理:', id)
      }
      return code
    }
  }
}
configResolved(config) {
  this.isDev = config.command === 'serve'
  this.isProduction = !this.isDev
},

load(id) {
  if (id === virtualModuleId) {
    return this.isDev
      ? `export const mode = 'development'`
      : `export const mode = 'production'`
  }
}

3.2 开发阶段钩子

  1. configureServer 开发服务器的钩子

    • 添加新的功能比如中间件 或者文件监听
configureServer(server: ViteDevServer): void | (() => void)

参数 server 包含以下重要属性:

  • middlewares: Connect 中间件实例
  • httpServer: 底层 HTTP 服务器
  • watcher: 文件监听器
  • ws: WebSocket 服务器
  • transformRequest(): 用于转换模块内容

eg

import type { Plugin } from 'vite'
import type { ViteDevServer } from 'vite'

export default function vitePluginTest(): Plugin {
  let devServer: ViteDevServer

  return {
    name: 'vite-plugin-test',
    configureServer(server) {
      devServer = server

      // 监听src目录下的所有文件变化(修正了watcher.add的用法)
      server.watcher.add('src/**/*')

      // 添加API接口
      server.middlewares.use('/api/data', (_, res) => {
        res.setHeader('Content-Type', 'application/json')
        res.end(
          JSON.stringify({
            data: '测试数据',
            timestamp: Date.now()
          })
        )
      })

      // 请求日志中间件
      server.middlewares.use((req, res, next) => {
        console.log(`[${new Date().toISOString()}] 请求: ${req.method} ${req.url}`)
        next()
      })

      // 监听文件变化(修正了监听逻辑)
      server.watcher.on('change', (file) => {
        console.log(`文件发生变化: ${file}`)
        if (file.startsWith('src/')) {
          console.log('src目录文件变化,刷新页面')
          server.ws.send({
            type: 'full-reload',
            path: '*'
          })
        }
      })
    },
    handleHotUpdate({ file }) {
      if (file.endsWith('.tsx')) {
        console.log('TSX文件修改,触发全量刷新')
        devServer.ws.send({
          type: 'full-reload',
          path: '*'
        })
      }
    }
  }
}

注意

configureServer(server) {
  // 这个会在Vite中间件之前执行
  server.middlewares.use(...)
  
  // 返回的函数会在Vite中间件之后执行
  return () => {
    server.middlewares.use(...)
  }
}
  1. transformIndexHtml 可以定制html内容

可以 自动插入标签 修改内容

 transformIndexHtml(html, ctx) {
      const tags: Array<{
        tag: string
        injectTo: 'head' | 'body' | 'head-prepend' | 'body-prepend'
        children?: string
        attrs?: Record<string, string>
      }> = []

      const isDev = !!ctx.server
      if (isDev) {
        tags.push({
          tag: 'script',
          injectTo: 'body-prepend',
          children: 'console.log("开发模式已启动")'
        })
      } else {
        tags.push({
          tag: 'meta',
          injectTo: 'head',
          attrs: { name: 'robots', content: 'index,follow' }
        })
      }

      const newHtml = html.replace(
        '<title>Vite + React + TS</title>',
        '<title>我的定制应用</title>'
      )

      // 返回符合 Vite 要求的类型
      return {
        html: newHtml,
        tags
      }
    },
  1. handleHotUpdate模块热更新
    handleHotUpdate(ctx) {
      if (ctx.file.endsWith('.tsx')) {
        console.log('TSX文件修改,触发全量刷新')
        devServer.ws.send({
          type: 'full-reload',
          path: '*'
        })
      }

      // 2. 只处理项目文件,忽略node_modules
      return ctx.modules.filter((module) => !module?.id?.includes('node_modules'))
    }
graph TD
    A[采购食材 config] --> B[确认菜单 configResolved]
    B --> C[员工培训 configureServer]
    C --> D[开门营业 buildStart]
    D --> E{客人点餐}
    E -->|HTML订单| F[摆盘 transformIndexHtml]
    E -->|食材请求| G[接单→备货→烹饪 resolveId→load→transform]
    E -->|变更需求| H[厨房监控 handleHotUpdate]
    H --> I{是否打烊}
    I -->|是| J[清理 buildEnd → 关店 closeBundle]

4. 例子

4.1 自动引入antd组件

github.com/unplugin/un…

一个自动引入插件 可以自动为你的项目按需导入 API,无需手动编写 import 语句,自动导入,按需加载。

  • resolvers 是 unplugin-auto-import 中的一个高级配置选项,用于自定义解析自动导入的组件或工具函数的方式。

思考: 问题

  • 我们如果开发的时候 使用antd组件 可以使用这个插件 导入很多antd组件,让vite.config文件内容很多 不太美观
  • 并且antd组件中没有默认的字母开头 比如 Button 可能会造成与其他自定义组件冲突,想一下是不是可以给antd 组件加个前缀 就像AButton

所以的插件功能主要实现 自动导入全部antd组件 并且可以自定义前缀

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import AutoImport from 'unplugin-auto-import/vite'
import antdResolver from './unplugin-auto-import-antd'

// https://vite.dev/config/
export default defineConfig({
  plugins: [
    react(),
    AutoImport({
      imports: [
        'react',
        {
          // antd: [
          //   'Button',
          //   'Input'
          // ]
        }
      ],
      resolvers: [
        // antdResolver({
        //   prefix: 'A', // 可选:为所有组件添加 A 前缀
        //   packageName: 'antd' // 可选:默认为 'antd'
        // }),
        {
          type: 'component',
          resolve: (name: string) => {
            console.log('resolve', name)
            const supportedComponents = ['AButton', 'Button', 'AInput', 'Table'] // 扩展这个列表

            if (supportedComponents.includes(name)) {
              return {
                from: 'antd',
                name: 'Input',
                as: `${name}` // 统一添加A前缀
              }
            }
            return undefined
          }
        }
      ],
      dts: true, // 生成类型声明文件
      eslintrc: {
        enabled: true // 生成 eslint 配置
      }
    })
  ]
})


4.2 svg直接作为组件导入

import type { Plugin } from 'vite'
import fs from 'node:fs/promises'
import { transform } from '@svgr/core'
import { transform as esbuildTransform } from 'esbuild'

interface SvgrOptions {
  defaultExport?: 'url' | 'component' // 导出类型:URL字符串 或 React组件
  svgrOptions?: Record<string, any>   // 自定义SVGR配置
}

export default function svgrPlugin(options: SvgrOptions = {}): Plugin {
  // 设置默认值:默认导出为组件,空SVGR配置
  const { defaultExport = 'component', svgrOptions = {} } = options

  return {
    name: 'vite-plugin-svgr',
    
    // transform 钩子:转换文件内容
    async transform(_, id) {
      // 只处理 .svg 文件
      if (!id.endsWith('.svg')) return
      
      try {
        // 1. 读取 SVG 文件内容
        const svg = await fs.readFile(id, 'utf-8')
        
        // 2. 使用 SVGR 将 SVG 转换为 React 组件代码
        const componentCode = await transform(
          svg, // SVG 原始内容
          {
            ...svgrOptions, // 用户自定义配置
            
            // 核心插件配置开始
            plugins: [
              '@svgr/plugin-jsx',      // 转换 SVG 为 JSX
              '@svgr/plugin-prettier'   // 格式化生成的代码
            ],
            
            // 其他重要配置
            typescript: true,          // 生成 TS 兼容代码
            jsxRuntime: 'automatic',   // 使用新版 JSX 运行时
            exportType: 'named',       // 使用命名导出
            
            // 自定义模板:控制组件输出结构
            template: ({ componentName, jsx }, { tpl }) => {
              return tpl`
                const ${componentName} = (props) => ${jsx};
                export { ${componentName} };
              `
            }
          },
          { componentName: 'ReactComponent' } // 设置组件名称
        )
        
        // 3. 清理生成的代码
        let jsCode = componentCode
          .replace(/^\/\*.*?\*\/\s*/gms, '') // 移除注释
          .replace(/\n+/g, '\n')            // 压缩空行
          .trim()
        
        // 4. 处理导出逻辑
        if (defaultExport === 'url') {
          // URL 模式:默认导出 SVG 路径
          jsCode = `
            ${jsCode}
            export default ${JSON.stringify(id)};
          `.trim()
        } else {
          // 组件模式:默认导出 React 组件
          jsCode = `
            ${jsCode}
            export default ReactComponent;
          `.trim()
        }
        
        // 5. 使用 esbuild 转换 JSX 为浏览器可执行代码
        const result = await esbuildTransform(jsCode, {
          loader: 'jsx',      // 指定为 JSX 类型
          jsx: 'automatic',   // 使用新版 JSX 转换
          sourcefile: id,     // 源文件路径(用于 sourcemap)
          format: 'esm',      // 输出 ESM 格式
          target: 'es2020',   // 目标 ES 版本
          logLevel: 'silent'  // 不输出日志
        })
        
        // 6. 返回转换后的代码
        return {
          code: result.code,
          map: result.map || null
        }
      } catch (error) {
        // 错误处理:回退到原始 SVG 路径导出
        console.error(`SVG转换失败 [${id}]:`, error)
        return {
          code: `export default ${JSON.stringify(id)};`,
          map: null
        }
      }
    }
  }
}

Web开发中的文件上传

2025年7月6日 16:37

在Web开发中,文件上传是一个常见的功能需求。无论是用户头像的上传、文档的提交还是图片的分享,文件上传都扮演着重要的角色。本文将详细介绍文件上传的消息格式和实现方式,帮助你更好地理解和实现文件上传功能。

一、文件上传的消息格式

文件上传的本质仍然是一个数据提交过程,只是数据量通常较大。在实践中,人们逐渐形成了一种共识,文件上传默认使用multipart/form-data格式。这种格式允许在一个请求中发送多个部分(part),每个部分可以包含不同的数据类型,如文本字段和文件数据。

请求格式示例

以下是一个典型的文件上传请求格式:

POST /upload HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW

----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="avatar"; filename="小仙女.jpg"
Content-Type: image/jpeg

(文件二进制数据)
----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="username"

admin
----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="password"

123123
----WebKitFormBoundary7MA4YWxkTrZu0gW

关键点说明

  • 请求方法:文件上传通常使用POST请求。
  • Content-Typemultipart/form-data,浏览器会自动分配一个定界符boundary
  • 请求体格式:请求体被boundary分割成多个部分,每个部分是一个键值对。对于文件数据部分,还会包含文件的本地名称和MIME类型。

二、文件上传的实现

在现代Web开发中,文件上传通常通过表单或JavaScript实现。以下是一个基于表单的文件上传实现示例:

HTML表单实现

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>文件上传示例</title>
</head>
<body>
    <form action="http://localhost:8000/api/upload" method="post" enctype="multipart/form-data">
        <label for="avatar">选择文件:</label>
        <input type="file" id="avatar" name="avatar">
        <br>
        <label for="username">用户名:</label>
        <input type="text" id="username" name="username">
        <br>
        <label for="password">密码:</label>
        <input type="password" id="password" name="password">
        <br>
        <button type="submit">上传文件</button>
    </form>
</body>
</html>

JavaScript实现

使用JavaScript可以实现更灵活的文件上传功能,例如在上传前进行文件大小和类型的验证。以下是一个使用FormDatafetch API实现文件上传的示例:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>文件上传示例</title>
</head>
<body>
    <input type="file" id="avatar">
    <input type="text" id="username">
    <input type="password" id="password">
    <button id="uploadButton">上传文件</button>

    <script>
        document.getElementById('uploadButton').addEventListener('click', function() {
            const avatar = document.getElementById('avatar').files[0];
            const username = document.getElementById('username').value;
            const password = document.getElementById('password').value;

            const formData = new FormData();
            formData.append('avatar', avatar);
            formData.append('username', username);
            formData.append('password', password);

            fetch('http://localhost:8000/api/upload', {
                method: 'POST',
                body: formData
            })
            .then(response => response.json())
            .then(data => {
                console.log('上传成功:', data);
            })
            .catch(error => {
                console.error('上传失败:', error);
            });
        });
    </script>
</body>
</html>

Blob 对象及 Base64 转换指南

作者 XXUZZWZ
2025年7月6日 16:20

Blob 对象及 Base64 转换指南

一、Blob 对象详解

核心概念

Blob(Binary Large Object) 是 JavaScript 处理二进制数据的核心对象:

  • 不可变性:创建后内容不可修改(可通过切片创建新 Blob)
  • 类型标识:通过 MIME 类型(如image/png)描述数据格式
  • 大文件支持:专为处理浏览器中的大型二进制数据设计

主要应用场景

  1. 文件操作<input type="file">获取的 File 对象继承自 Blob

    document.querySelector("input").addEventListener("change", (e) => {
      const file = e.target.files[0]; // File继承自Blob
      console.log(file.size); // 文件大小
    });
    
  2. 动态文件下载

    const blob = new Blob(["Hello, World!"], { type: "text/plain" });
    const url = URL.createObjectURL(blob);
    
    const a = document.createElement("a");
    a.href = url;
    a.download = "file.txt";
    a.click();
    
    URL.revokeObjectURL(url); // 释放内存
    
  3. 分片上传

    async function upload(file) {
      const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
      for (let start = 0; start < file.size; start += CHUNK_SIZE) {
        const chunk = file.slice(start, start + CHUNK_SIZE);
        await fetch("/upload", { method: "POST", body: chunk });
      }
    }
    
  4. 媒体预览

    const img = document.getElementById("preview");
    img.src = URL.createObjectURL(file);
    

二、关键 API

构造函数

new Blob(array, options)

  • array:数据源数组(ArrayBuffer/String/Blob)
  • options
    • type:MIME 类型(默认""

实例方法

blob.size; // 字节大小
blob.type; // MIME类型
blob.slice(); // 分割Blob
blob.stream(); // 返回ReadableStream
blob.arrayBuffer(); // 转为ArrayBuffer(异步)
blob.text(); // 转为文本(异步)

三、Base64 转 Blob 实践

转换函数实现

function base64ToBlobUrl(base64Data, mimeType) {
  // 1. Base64解码
  const byteChars = atob(base64Data);

  // 2. 创建二进制数组
  const byteArray = new Uint8Array(byteChars.length);
  for (let i = 0; i < byteChars.length; i++) {
    byteArray[i] = byteChars.charCodeAt(i);
  }

  // 3. 创建Blob并生成URL
  const blob = new Blob([byteArray], { type: mimeType });
  return URL.createObjectURL(blob);
}

使用示例

// 音频播放
const audioUrl = base64ToBlobUrl(base64Data, "audio/mpeg");
const audio = new Audio(audioUrl);
audio.play();

// 图片预览
const img = document.createElement("img");
img.src = base64ToBlobUrl(base64Image, "image/jpeg");
document.body.appendChild(img);

五大核心优势

  1. 内存优化:避免 Base64 的 33%体积膨胀
  2. 流式处理:支持边加载边播放
  3. 安全合规:规避 CSP 对data:协议的限制
  4. 隐私保护:敏感数据无需网络传输
  5. 性能提升:本地转换减少服务器压力

四、关键注意事项

  1. 内存管理:及时释放资源

    const url = base64ToBlobUrl(data, "audio/mp3");
    audio.src = url;
    
    // 播放结束后释放
    audio.onended = () => URL.revokeObjectURL(url);
    
  2. MIME 类型对照表

    实际格式 正确 MIME 类型
    MP3 audio/mpeg
    WAV audio/wav
    PNG image/png
    JPEG image/jpeg
  3. 大文件处理优化

    function chunkedBase64ToBlob(base64Data, mimeType, chunkSize = 1_000_000) {
      const chunks = [];
      for (let i = 0; i < base64Data.length; i += chunkSize) {
        const chunk = base64Data.slice(i, i + chunkSize);
        const byteChars = atob(chunk);
        const byteArray = new Uint8Array(byteChars.length);
        for (let j = 0; j < byteChars.length; j++) {
          byteArray[j] = byteChars.charCodeAt(j);
        }
        chunks.push(byteArray);
      }
      return URL.createObjectURL(new Blob(chunks, { type: mimeType }));
    }
    

五、实际应用场景

语音消息处理

// 接收Base64语音
socket.on("voice-message", (data) => {
  const audio = new Audio(base64ToBlobUrl(data, "audio/webm"));

  // 播放后自动清理
  audio.onended = () => {
    URL.revokeObjectURL(audio.src);
    audio.remove();
  };

  document.body.appendChild(audio);
  audio.play();
});

Canvas 图像导出

// Canvas转Blob预览
canvas.toBlob((blob) => {
  const img = new Image();
  img.src = URL.createObjectURL(blob);
  img.onload = () => URL.revokeObjectURL(img.src);
  document.body.appendChild(img);
}, "image/png");

六、浏览器兼容方案

// 旧浏览器兼容处理
if (typeof atob === "undefined") {
  window.atob = function (encoded) {
    return Buffer.from(encoded, "base64").toString("binary");
  };
}

if (!URL.createObjectURL) {
  URL.createObjectURL = function (blob) {
    return `data:${blob.type};base64,${btoa(
      new Uint8Array(blob).reduce(
        (data, byte) => data + String.fromCharCode(byte),
        ""
      )
    )}`;
  };
}

最佳实践:Blob 对象是浏览器端处理二进制数据的基石,结合 Base64 转换技术,可在保证性能的前提下实现丰富的文件操作功能。关键要掌握内存管理、类型匹配和分块处理三大核心技巧。

Vue 响应式系统全面解析:从基础到高级实践

作者 markyankee101
2025年7月6日 16:14

Vue 响应式系统全面解析:从基础到高级实践

文章结构如下:

  1. 引言:响应式系统的重要性
  2. 响应式基础:定义和核心概念
  3. Vue 2的响应式原理(Object.defineProperty)
  4. Vue 3的响应式原理(Proxy)
  5. 依赖收集与派发更新
  6. 响应式API对比(Vue 2 vs Vue 3)
  7. 计算属性和侦听器
  8. 响应式系统的局限性和解决方案
  9. 最佳实践
  10. 总结

引言:响应式编程的魅力

在现代前端开发中,响应式编程已成为构建动态用户界面的核心范式。Vue 的响应式系统是其最强大的特性之一,它使得数据与DOM之间的同步变得自动化高效。本文将深入剖析 Vue 响应式系统的实现原理、核心概念和最佳实践,帮助您全面掌握这一关键技术。

一、响应式基础概念

1.1 什么是响应式?

  • 定义:当数据发生变化时,依赖该数据的视图自动更新
  • 核心目标:数据驱动视图 (Data-Driven View)
  • 开发体验:开发者只需关注数据状态,无需手动操作 DOM

1.2 Vue 响应式核心要素

概念 作用 示例
响应式数据 被 Vue 追踪的数据 data(), reactive(), ref()
依赖收集 建立数据-视图关系 getter 中收集 watcher
派发更新 数据变化时通知更新 setter 中触发 watcher 更新
异步队列 优化更新性能 批量处理数据变更

1.3 响应式数据创建方式

// Vue 2 选项式 API
data() {
  return {
    message: 'Hello Vue 2'
  }
}

// Vue 3 组合式 API
import { reactive, ref } from 'vue'

const state = reactive({
  count: 0
})

const message = ref('Hello Vue 3')

二、Vue 2 响应式原理深度剖析

2.1 Object.defineProperty 实现

function defineReactive(obj, key, val) {
  const dep = new Dep() // 依赖管理器
  
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      // 收集当前正在计算的 watcher
      if (Dep.target) {
        dep.depend()
      }
      return val
    },
    set(newVal) {
      if (newVal === val) return
      val = newVal
      // 通知所有订阅者更新
      dep.notify()
    }
  })
}

2.2 依赖收集与派发更新流程

1. 组件渲染 → 2. 触发数据 getter → 3. 收集当前 Watcher
4. 数据变更 → 5. 触发 setter → 6. 通知 Dep → 7. 执行 Watcher 更新

2.3 Vue 2 响应式局限性

  1. 对象属性增删:无法检测 this.obj.newProp = value
  2. 数组索引修改this.arr[index] = newValue 不会触发更新
  3. 数组长度修改this.arr.length = newLength 无效
  4. 性能开销:深层嵌套对象递归转换性能较差

2.4 解决方案

  • Vue.set(target, propertyName/index, value)
  • this.$set(target, propertyName/index, value)
// 添加新属性
Vue.set(this.obj, 'newProp', value)
this.$set(this.obj, 'newProp', value)

// 修改数组
this.$set(this.arr, index, newValue)
this.arr.splice(index, 1, newValue)
1. 对象属性添加/删除
  • 问题:直接添加/删除属性不会触发更新

  • 解决

    // Vue 2
    this.$set(this.user, 'newProp', value)
    
    // Vue 3
    this.user.newProp = value  // 直接工作
    
2. 数组变化检测
  • 变异方法(自动触发更新): push(), pop(), shift(), unshift(), splice(), sort(), reverse()

  • 非变异方法filter(), concat(), slice() - 需要替换原数组

  • 特殊案例

    // Vue 2
    this.$set(this.items, index, newValue)
    
    // 直接修改长度不触发
    this.items.splice(newLength)
    

三、Vue 3 响应式革命:基于 Proxy

3.1 Proxy 的优势

  1. 全面拦截:支持对象/数组的所有操作类型
  2. 性能提升:按需响应,避免递归初始化
  3. 更好的API:支持 Map、Set、WeakMap 等集合类型

3.2 核心实现

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      track(target, key) // 依赖收集
      const res = Reflect.get(target, key, receiver)
      // 深层响应处理
      return isObject(res) ? reactive(res) : res
    },
    set(target, key, value, receiver) {
      const oldValue = target[key]
      const result = Reflect.set(target, key, value, receiver)
  
      if (hasChanged(value, oldValue)) {
        trigger(target, key) // 触发更新
      }
      return result
    },
    deleteProperty(target, key) {
      const hadKey = hasOwn(target, key)
      const result = Reflect.deleteProperty(target, key)
      if (hadKey) {
        trigger(target, key)
      }
      return result
    }
  })
}

3.3 Vue 3 响应式 API 家族

API 适用场景 特点 示例
reactive 对象/数组 深度响应 const state = reactive({ count: 0 })
ref 基本类型 通过.value 访问 const count = ref(0)
computed 计算属性 自动缓存 const doubled = computed(() => count.value * 2)
watch 侦听变化 精确控制 watch(count, (newVal) => {...})
watchEffect 自动侦听 立即执行 watchEffect(() => console.log(count.value))
toRefs 解构响应 保持响应性 const { count } = toRefs(state)

四、响应式系统核心机制

4.1 依赖收集系统

1751706713245.png

4.2 派发更新流程

1751706745463.png

4.3 异步更新队列

Vue 使用异步更新策略优化性能:

  • 同一事件循环内的数据变化会批量处理
  • nextTick 等待 DOM 更新后执行代码
this.count = 10
this.count = 20
this.count = 30 // 只会触发一次更新

// 获取更新后 DOM
nextTick(() => {
  console.log('DOM updated!')
})

五、高级响应式模式

5.1 自定义响应式逻辑

import { customRef } from 'vue'

function debouncedRef(value, delay = 200) {
  let timer
  return customRef((track, trigger) => {
    return {
      get() {
        track()
        return value
      },
      set(newValue) {
        clearTimeout(timer)
        timer = setTimeout(() => {
          value = newValue
          trigger()
        }, delay)
      }
    }
  })
}

// 使用
const text = debouncedRef('', 500)

5.2 响应式状态机

import { reactive } from 'vue'

const stateMachine = reactive({
  state: 'idle',
  transitions: {
    idle: { start: 'loading' },
    loading: { success: 'success', error: 'error' },
    success: { reset: 'idle' },
    error: { retry: 'loading', reset: 'idle' }
  },
  dispatch(action) {
    const nextState = this.transitions[this.state][action]
    if (nextState) this.state = nextState
  }
})

5.3 跨组件状态共享

// store.js
import { reactive } from 'vue'

export const store = reactive({
  count: 0,
  increment() {
    this.count++
  }
})

// ComponentA.vue
import { store } from './store'

// ComponentB.vue
import { store } from './store'

六、响应式系统最佳实践

6.1 性能优化策略

  1. 扁平化数据结构:避免深层嵌套
  2. 冻结静态数据Object.freeze(largeData)
  3. 虚拟滚动:处理大型列表
  4. 合理使用计算属性:缓存计算结果
  5. 按需监听:避免不必要的深度监听

6.2 常见陷阱与解决方案

问题场景 解决方案
循环引用导致内存泄漏 使用 WeakMap 或手动解引用
大型响应式对象性能问题 分片加载 + 非响应式处理
异步更新导致状态不一致 使用 nextTick 确保更新完成
第三方库集成问题 使用 shallowRef 或 markRaw

6.3 响应式设计原则

  1. 单一数据源:保持状态来源唯一性
  2. 不可变数据:复杂状态使用 immutable.js
  3. 关注点分离:状态管理与组件逻辑解耦
  4. 响应式隔离:组件内局部状态使用 ref/reactive
  5. 严格模式:开发环境下启用 Vue.config.warnHandler

七、响应式系统演进与未来

7.1 Vue 2 到 Vue 3 的演进

特性 Vue 2 Vue 3
核心实现 Object.defineProperty Proxy
数组处理 特殊劫持方法 原生支持
性能 递归初始化 惰性响应
集合类型 不支持 Map/Set 支持
按需响应 困难 精细控制

7.2 响应式未来趋势

  1. 更细粒度响应:Vue 3.4 引入 reactivity transform
  2. 服务端渲染优化:同构响应式处理
  3. WebAssembly 集成:高性能计算支持
  4. 跨框架标准:探索响应式规范统一化

结语:掌握响应式艺术

Vue 的响应式系统将前端开发从繁琐的 DOM 操作中解放出来,让开发者能够专注于数据和业务逻辑。通过本文的深度解析,您应该能够:

  1. 理解 Vue 响应式系统的核心原理
  2. 掌握 Vue 2 和 Vue 3 的响应式实现差异
  3. 熟练使用各种响应式 API 解决实际问题
  4. 应用最佳实践优化应用性能
  5. 避免常见的响应式陷阱

响应式编程不仅是 Vue 框架的特性,更是一种现代化的编程范式。掌握它,将为您构建高效、可维护的前端应用奠定坚实基础。

智能前端实践之 shot-word demo

作者 XXUZZWZ
2025年7月6日 15:51

shot-word demo

概述;利用 React 框架 实现上传图片,调用 moon-shoot 多模态模型识别图片,并通过设计 prompt 生成返回一个 json 数据,并调用火山引擎语音模型生成对应的语音。

数据设计

  1. 用户上传的图片 image
  2. 图片多模态模型识别结果 data,里面有页面渲染需要的所有数据。 { "image_discription": "图片描述", "representative_word": "图片代表的英文单词", "example_sentence": "结合英文单词和图片描述,给出一个简单的例句", "explaination": "结合图片解释英文单词,段落以 Look at...开头,将段落分句,每一句单独一行,解释的最后给一个日常生活有关的问句", "explaination_replys": ["根据 explaination 给出的回复 1", "根据 explaination 给出的回复 2"] }
  3. 例句语音,请求到的 explaination 的音频

页面结构

一个图片上传卡片,一个点击查看详情页面的按钮,以及点击按钮出现的详情页面,页面垂直排布,使用 flex 布局

事件处理

  1. 处理图片上传事件
  2. 点击查看详情按钮展开详情页和收起详情页

组件结构

  • APP
    • PictureCard

状态管理

  1. 图片上传状态
  2. 详情页展开收起状态
  3. Audio 是否请求到音频

效果

image.png

image.png

项目地址 AIFullStackJourney/React/shot-word at main · XXUZZWZ/AIFullStackJourney

难点

在前端将 base64 编码的图片数据转换成二进制对象,并使用 URL.createObjectURL()方法创建一个临时 URL,并设置给 img 标签的 src 属性,实现图片的预览。避免 Content-Security-Policydata: 协议的限制,规避跨域资源访问问题(CORS)。 Blob URL 可流式播放,无需等待完整数据解码 内存管理更优(Base64 Data URL 体积增大 33%)

实现函数

const getAudioUrl = (base64Data) => {
  // 创建一个数组来存储字节数据
  var byteArrays = [];
  // 使用atob()将Base64编码的字符串解码为原始二进制字符串
  // atob: ASCII to Binary String (原始二进制字符串(每个字符表示一个字节,但以字符串的形式存在))
  var byteCharacters = atob(base64Data);
  // 遍历解码后的二进制字符串的每个字符
  for (var offset = 0; offset < byteCharacters.length; offset++) {
    // 将每个字符转换为其ASCII码值(0-255之间的数字)
    var byteArray = byteCharacters.charCodeAt(offset);
    // 将ASCII码值添加到字节数组中
    byteArrays.push(byteArray);
  }
  // 创建一个Blob对象
  // new Uint8Array(byteArrays)将普通数组转换为8位无符号整数数组
  // { type: 'audio/mp3' } 指定Blob的MIME类型为MP3音频

  var blob = new Blob([new Uint8Array(byteArrays)], { type: "audio/mp3" });
  // 使用URL.createObjectURL创建一个临时的URL
  // 这个URL可以用于<audio>标签的src属性
  // 这个URL在当前页面/会话有效,页面关闭后会自动释放
  // 创建一个临时 URL 供音频播放
  return URL.createObjectURL(blob);
};

下面我将详细拆解 getAudioUrl 函数的每个步骤,解释其工作原理和前端处理的意义:

函数流程详解

const getAudioUrl = (base64Data) => {
  // 步骤1:准备存储容器
  var byteArrays = [];

作用:创建空数组用于存储转换后的二进制数据。


// 步骤2:Base64解码
var byteCharacters = atob(base64Data);

关键点

  • atob() 是浏览器内置函数,将 Base64 字符串解码为二进制字符串
  • Base64 是文本表示形式,实际音频需要二进制格式
  • 示例:"TWFu" (Base64) → "Man" (二进制字符串)

// 步骤3:字符到字节转换
for (var offset = 0; offset < byteCharacters.length; offset++) {
  var byteArray = byteCharacters.charCodeAt(offset);
  byteArrays.push(byteArray);
}

核心操作

  1. 遍历每个二进制字符
  2. charCodeAt() 获取字符的 ASCII 码值(0-255)
  3. 将数值存入数组
    内存图示
字符:  'M'     'a'     'n'
ASCII: 77      97      110
数组: [77, 97, 110]

// 步骤4:创建二进制对象
var blob = new Blob([new Uint8Array(byteArrays)], { type: "audio/mp3" });

关键技术

  1. new Uint8Array(byteArrays)
    • 将普通数组转为二进制数组(每个元素占 1 字节)
    • 内存效率高于普通数组
  2. new Blob()
    • 创建不可变的二进制大对象
    • { type: "audio/mp3" } 指定 MIME 类型,告知浏览器这是 MP3 音频

  // 步骤5:生成可访问URL
  return URL.createObjectURL(blob);
};

核心价值

  • 创建指向内存 Blob 的临时 URL(格式:blob:https://example.com/550e8400-e29b-41d4...
  • 可直接用于 HTML 媒体元素:
    <audio src="{getAudioUrl(base64Data)}" controls />
    

为何需要这样处理?

  1. 数据转换的必要性

    • Base64 是文本格式(ASCII),音频需要二进制格式
    • 浏览器无法直接播放 Base64 字符串
  2. 与 Data URL 的对比

    // 替代方案(不推荐):
    const dataUrl = `data:audio/mp3;base64,${base64Data}`;
    
    • Blob URL 优势
      • 支持流式播放(无需完整加载)
      • 内存效率高(Base64 体积大 33%)
      • 可手动释放内存
  3. 浏览器兼容性处理

    • 旧版 IE 需 polyfill:
      // 兼容旧浏览器的atob
      if (typeof atob === "undefined") {
        atob = function (encoded) {
          return Buffer.from(encoded, "base64").toString("binary");
        };
      }
      

实际应用场景

语音消息预览

// 接收Base64语音数据
socket.on("voice-message", (data) => {
  const audio = new Audio(getAudioUrl(data));
  audio.play();

  // 播放后清理内存
  audio.onended = () => URL.revokeObjectURL(audio.src);
});

性能优化技巧

// 更高效的Uint8Array直接填充
const optimizedVersion = (base64Data) => {
  const str = atob(base64Data);
  const array = new Uint8Array(str.length);

  for (let i = 0; i < str.length; i++) {
    array[i] = str.charCodeAt(i);
  }

  return URL.createObjectURL(
    new Blob([array], { type: "audio/webm" }) // 根据实际类型调整
  );
};

关键注意事项

  1. 内存管理

    • 主动释放资源:

      const url = getAudioUrl(data);
      audio.src = url;
      
      // 不再需要时
      URL.revokeObjectURL(url);
      
    • 页面关闭时浏览器自动回收

  2. MIME 类型陷阱

    • audio/mp3 可能不匹配实际编码
    • 正确设置类型:
      实际格式 正确 MIME 类型
      MP3 audio/mpeg
      WAV audio/wav
      OGG audio/ogg
  3. 大文件处理

    • 超过 50MB 的音频建议分块处理:

      const CHUNK_SIZE = 1_000_000; // 1MB分块
      const chunks = [];
      
      for (let i = 0; i < base64Data.length; i += CHUNK_SIZE) {
        const chunk = base64Data.slice(i, i + CHUNK_SIZE);
        chunks.push(atob(chunk));
      }
      
      const blob = new Blob(
        chunks.map(
          (c) => new Uint8Array([...c].map((char) => char.charCodeAt(0)))
        ),
        { type: "audio/mp3" }
      );
      

这种处理方式在前端实现了 二进制转换 → 内存存储 → 媒体资源生成 的完整链路,是处理客户端音频数据的标准实践方案。

在前端处理 Base64 音频数据并生成 Blob URL(如示例代码所示)具有以下显著优势:

1. 减少服务器负担

  • 直接在浏览器完成数据转换,避免将 Base64 数据上传到服务器再生成文件
  • 节省服务器计算资源和存储空间

2. 提升响应速度

  • 省去网络往返时间(RTT),音频数据转换后立即可播放
  • 特别适合实时场景(如语音消息即时播放)

3. 增强隐私性

  • 敏感音频数据无需通过网络传输,降低中间环节泄露风险
  • 临时 URL 仅在当前页面生命周期有效(页面关闭自动释放)

4. 优化资源加载

<!-- 直接使用生成的 URL -->
<audio src="{getAudioUrl(base64Data)}" controls />
  • 比 Base64 Data URL(data:audio/mp3;base64,...)更高效:
    • Blob URL 可流式播放,无需等待完整数据解码
    • 内存管理更优(Base64 Data URL 体积增大 33%)

5. 跨域安全优势

  • 避免 Content-Security-Policydata: 协议的限制
  • 规避跨域资源访问问题(CORS)

6. 内存效率

  • 使用 Uint8Array 直接处理二进制数据,比字符串操作更高效
  • 临时 URL 可手动释放资源:
    // 播放结束后释放资源
    audioElement.onended = () => {
      URL.revokeObjectURL(audioElement.src);
    };
    

潜在注意事项

  • 浏览器兼容性:现代浏览器均支持,但需注意:
    • IE10+ 支持 Blob,IE11+ 支持 URL.createObjectURL
    • 旧版安卓需 polyfill(如 core-js
  • 大文件处理:超过 500MB 的音频可能引发内存问题
  • MIME 类型:确保 type 与实际格式匹配(示例中 audio/mp3 需与实际编码一致)

性能优化建议

// 更高效的转换方式(减少循环次数)
const getAudioUrlOptimized = (base64Data) => {
  const byteChars = atob(base64Data);
  const byteArray = new Uint8Array(byteChars.length);

  for (let i = 0; i < byteChars.length; i++) {
    byteArray[i] = byteChars.charCodeAt(i);
  }

  return URL.createObjectURL(new Blob([byteArray], { type: "audio/mp3" }));
};

典型应用场景

  1. 即时通讯中的语音消息预览
  2. 网页版音频编辑器(本地处理)
  3. FileReader.readAsDataURL() 获取的音频处理
  4. 免服务器的 Web Audio API 应用

⚠️ 重要提示:对于持久化存储,仍需将原始 Base64 或二进制数据发送至服务器,Blob URL 仅适用于临时访问场景。

四、Cypress测试框架拆解

2025年7月6日 15:50

Cypress测试框架拆解

前面咱们已经跑通了第一个Cypress测试用例,这一章就来深入看看它的“内部构造”。就像拆一台家用小电器,搞懂每个部件的作用,才能更好地驾驭它解决实际问题。

一、默认文件结构:Cypress的“五脏六腑”

第一次启动Cypress时,它会自动生成一套标准的文件结构,就像盖房子先搭好框架。这些文件夹各司其职,让测试工作条理清晰:

  • cypress/fixtures:测试数据的“仓库”
    这里放的是JSON格式的静态数据文件,比如登录用的账号密码、接口返回的模拟数据等。举个例子,创建一个user.json,把用户名密码存进去,测试时用cy.fixture('user')就能直接调用,不用在代码里反复写这些数据,改起来也方便。

  • cypress/integration:测试用例的“主战场”
    所有测试脚本(.js、.jsx等格式)都放在这里,Cypress会自动识别并执行。咱们之前写的登录测试用例,就该放在这个文件夹下。如果项目大,还能建子文件夹分类,比如按“登录”“购物车”“支付”分开管理,找起来一目了然。

  • cypress/plugins:功能扩展的“工具箱”
    默认有个index.js文件,用来扩展Cypress的能力。比如想在测试前连接数据库清理数据,或者动态修改配置,都可以在这里写代码,Cypress会在测试开始前自动加载执行。

  • cypress/support:自定义命令的“加工厂”

    • commands.js:可以把重复的操作封装成命令,比如“输入用户名→输入密码→点击登录”这一串步骤,写成cy.login(),之后在所有测试用例里直接调用,不用重复写代码。
    • index.js:负责加载上面的自定义命令,还能放一些全局配置,比如设置每个用例运行前都自动访问某个页面。

二、自定义配置:给Cypress“调参数”

Cypress很灵活,就像手机的“设置”界面,能根据项目需求调整各种参数,主要通过cypress.json文件或代码来配置:

  • 基础配置项
    比如设置基础URL,之后访问页面就不用写完整地址了;调整浏览器窗口大小,模拟手机或平板的显示效果:

    {
      "baseUrl": "http://localhost:7077",  // 基础地址,简化访问路径
      "viewportWidth": 375,              // 模拟手机宽度
      "viewportHeight": 667,             // 模拟手机高度
      "testFiles": "login/**/*.js"       // 只运行登录相关的测试用例
    }
    
  • 动态改配置
    代码里用Cypress.config()能临时调整参数,比如测试某个慢接口时,延长超时时间:

    // 把页面加载超时从默认60秒改成100秒
    Cypress.config('pageLoadTimeout', 100000)
    

三、重试机制:Cypress的“智能等待”

写自动化测试最头疼的就是“元素还没加载出来,代码就执行完了”,导致测试失败。但Cypress有个贴心的“自动重试”功能,就像有人帮你盯着页面,等元素准备好了再执行操作:

比如这段代码,检查页面上的h1标签是否包含“欢迎回来”:

cy.get('h1').should('contain', '欢迎回来')

如果h1暂时没有这段文字,Cypress会在4秒内(可以通过defaultCommandTimeout调整)反复检查,直到文字出现才继续,不用手动写sleep等待。

不过要注意:只有查询元素的命令(比如get find)会重试,像click()这种可能改变页面状态的命令不会重试,避免重复操作出问题。

四、测试报告:给测试结果“出份成绩单”

测试跑完了,怎么清晰地展示结果?Cypress支持好几种“成绩单”格式,满足不同场景:

  • 命令行报告:默认直接在控制台显示,能看到每个用例的成功失败情况,适合本地调试。
  • JUnit格式:生成XML文件,方便Jenkins等持续集成工具读取,在流水线里展示测试结果。
  • Mochawesome报告:生成带截图的HTML报告,失败的步骤一目了然,适合分享给团队看。

生成报告很简单,运行时加个参数就行,比如生成HTML报告:

npx cypress run --reporter mochawesome

React 中 root.render 与 unmount 函数的流程

作者 june18
2025年7月6日 15:46

上一篇文章 创建根节点 createRoot 介绍了 createRoot 函数,本文继续说下它的返回值,共三个:

  1. _internalRoot
  2. render
  3. unmount

其中 _internalRoot 在上一篇文章已经介绍,这里重点讲述 renderunmount

render 渲染

ReactDOMRoot.prototype.render = function(children: ReactNodeList): void {
    const root = this._internalRoot;

    updateContainer(children, root, null, null)
}

unmount 卸载

ReactDOMRoot.prototype.unmount = function(): void {
  const root = this._internalRoot;

  if(root !== null) {
    this._internalRoot = null;
    const container = root.containerInfo;

    // flushSync 是 React 提供的 API,外部也可以用,参考 https://zh-hans.react.dev/reference/react-dom/flushSync
    flushSync((() => {
      updateContainer(null, root, null, null)
    }))
    unmarkContainerAsRoot(container) // 函数定义在上一篇文章中
  }
}

可以看到,两者都有 updateContainer 函数,下面重点说下。

updateContainer 把子节点渲染到容器内

updateContainer 函数分为 4 步:

// 把子节点渲染到容器内
function (
  element: ReactNodeList, // 子节点
  container: OpaqueRoot // FiberRoot
): Lane {
  // 1. 获取 current 和 lane
  // 在 React 中,lane 是用于标识 update 优先级,可以理解为表示 update 的优先级的一种机制。每个 update 都会被分配一个或多个 lane,以确定其在更新队列中的优先级顺序。
  const current = container.current // container 对应的 fiber
  // 获取本次 update 对应的 lane
  const lane = requestUpdateLane(current)
  
  // 2. 创建 update
  const update = createUpdate(lane)
  update.payload = {element}
  
  // 3. update 入队,将 update 加入到 fiber 的 updateQueue 中
  const root = enqueueUpdate(current, update, lane);
  if (root !== null) {
    // 4. 调度更新
    scheduleUpdateOnFiber(root, current, lane);
  }
}
requestUpdateLane
function requestUpdateLane(fiber: Fiber): Lane {
  // React 内部的一些 update,比如 flushSync、setState,会通过上下文变量来跟踪其优先级
  const updateLane: Lane = (getCurrentUpdatePriority(): any);
  if (updateLane !== NoLane) {
    return updateLane
  }

  // React 外部的 update,根据事件类型,向当前环境获取对应的优先级
  const eventLane: Lane = (getCurrentEventPriority(): any);
  return updateLane
}

function getCurrentEventPriority(): EventPriority {
  const currentEvernt = window.event;
  if (currentEvernt === undefined) {
    return DefaultEventPriority
  }

  return getEventPriority(currentEvernt.type)
}

export const DefaultEventPriority: EventPriority = DefaultLane  // 页面初次渲染的 lane 是二进制的 32

let currentUpdatePriority: EventPriority = NoLane

function getCurrentUpdatePriority(): EventPriority {
  return currentUpdatePriority
}

// 点击更新状态时触发
function setCurrentUpdatePriority(newPriority: EventPriority) {
  currentUpdatePriority = newPriority
}
createUpdate

createRoot(root).render() 节点与类组件的 setStateforceUpdate 阶段均会创建 update render() 两次会创建两个 update 类组件中调用两次 setState,也会创建两个 update

// const UpdateState = 0  // 页面初次渲染,类组件 setState
// const ReplaceState = 1 // 类组件
// const ForceState = 2 // 类组件
// const CaptureState = 3 // 类组件

type Update<State> = {
  lane: Lane,
  tag: 0 | 1 | 2 | 3,
  payload: any, // 携带的参数。初次渲染是子节点;类组件比如 setState 的参数
  callback: (() => nuxed) | null,
  next: Update<State> | null // 单链表
}

const UpdateState = 0
function createUpdate(lane: Lane): Update<mixed> {
  const update: Update<mixed> = {
    lane,
    tag: UpdateState,
    payload: null,
    callback: null,
    next: null
  }

  return update
}
enqueueUpdate

createRoot(root).render() 阶段与类组件的 setStateforceUpdate 阶段最开始调用的是 enqueueUpdate

type SharedQueue = {
  pending: Update<State> | null, // 单向循环链表,尾节点 -> 头节点
  lanes: Lanes // 多个 update 合并成一个
}

function enqueueUpdate<State>(
  fiber: Fiber,
  update: Update<State>,
  lane: Lane
) {
  const updateQueue = fiber.updateQueue
  if (updateQueue === null) {
    // 只发生在 fiber 卸载时
    return null
  }

  const sharedQueue: SharedQueue<State> = (updateQueue: any).shared

  return enqueueConcurrentClassUpdate(fiber, sharedQueue, update, lane)
}


type ConcurrentUpdate = {
  next: ConcurrentUpdate,
  lane: Lane
}

type ConcurrentQueue = {
  pending: ConcurrentUpdate | null
}

// 如果渲染正在进行中,并且收到来自并发事件的更新,我们会等到当前的渲染结束(无论是完成还是被中断)之后再将其添加到 fiber 队列中。
// 将其推送到这个数组中,这样我们以后就可以访问 queue、fiber、update 等。
const concurrentQueues: Array<any> = []
let concurrentQueuesIndex = 0;

let concrrentlyUpdatedLanes: Lanes = NoLanes;


function enqueueConcurrentClassUpdate<State>(
  fiber: Fiber,
  queue: ClassQueue<State>,
  update: ClassUpdate<State>,
  lane: Lane
): FiberRoot | null {
  const concurrentQueue: ConcurrentQueue = (queue: any)
  const concurrentUpdate: ConcurrentUpdate = (update: any)
  // 1. update 入队
  enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane)
  // 2. 返回 FiberRoot
  return getRootForUpdateFiber(fiber)
}

function enqueueUpdate(
  fiber: Fiber,
  queue: ConcurrentQueue | null,
  update: ConcurrentUpdate | null,
  lane: Lane
) {
  concurrentQueues[concurrentQueuesIndex++] = fiber
  concurrentQueues[concurrentQueuesIndex++] = queue
  concurrentQueues[concurrentQueuesIndex++] = update
  concurrentQueues[concurrentQueuesIndex++] = lane

  concrrentlyUpdatedLanes = mergeLanes(concrrentlyUpdatedLanes, lane)

  fiber.lanes = mergeLanes(fiber.lanes, lane)
  const alternate = fiber.alternate

  // 如果老节点非空
  if (alternate !== null) {
    alternate.lanes = mergeLanes(alternate.lanes, lane)
  }
}

function mergeLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes {
  return a | b
}
scheduleUpdateOnFiber

页面初次渲染、类组件 setState/forceUpdate、函数组件 useState/useReducer 都会走到更新,都会调用 scheduleUpdateOnFiber 函数。

function _markRootUpdated(root: FiberRoot, updatedLane: Lane) {
  root.pendingLanes |= updatedLane

  // 如果 update 是 idle 的,将不会处理它,因为我们直到所有常规 update 完成后才会处理 idle 更新
  if(updatedLane !== IdleLane) {
    root.suspendedLanes = NoLandex
    root.pingedLanes = NoLanes
  }
}

// 标记根节点有一个 pending update,即待处理的更新。
function markRootUpdated(root: FiberRoot, updatedLanes: Lanes) {
  _markRootUpdated(root, updatedLanes)

  if (enableInfiniteRenderLoopDetection) {
    // 如果循环超过限制次数(类组件 50 次,函数组件 25 次),抛出错误。比如在类组件的 render 函数里执行 setState
    throwIfInfiniteUpdateLoopDetected()
  }
}

function scheduleUpdateOnFiber(
  root: FiberRoot, // 从根节点开始更新:具体的更新是在子节点上发生的,但是遍历是从根节点开始的
  fiber: Fiber,
  lane: Lane
) {
  markRootUpdated(root, lane)
}
❌
❌