阅读视图

发现新文章,点击刷新页面。

案例+图解带你遨游 Canvas 2D绘图 Fabric.js🔥🔥(5W+字)

Fabric.js 简介

Fabric.js 是一个功能强大且操作简单的 Javascript HTML5 canvas 工具库。

00.png

『Fabric.js 官网首页』

『Fabric.js Demos』

本文主要讲解 Fabric.js 有基础也有实战,包括:

  • 画布的基本操作
  • 基础图形绘制方法(矩形、圆形、三角形、椭圆、多边形、线段等)
  • 自定义图形
  • 图片的使用
  • 文本和文本框
  • 图形和文本的基础样式
  • 渐变
  • 选中状态
  • 分组和取消分组
  • 动画
  • 设置和获取图形层级
  • 基础事件
  • 禁止水平、垂直移动
  • 缩放和平移画布
  • 视口坐标和画布坐标转换
  • 序列化和反序列化
  • ……

起步

1. 新建页面并引入 Fabric.js

import { fabric } from 'fabric'

2. 创建 canvas 容器

HTML 中创建 <canvas>,并设置容器的 id宽高,width/height

<canvas width="400" height="400" id="c" style="border: 1px solid #ccc;"></canvas>

这里创建了一个 canvas 容器,边框为灰色,id="c" 。指定长宽都为 400px

003.png

3. 使用 fabric 接管容器,并画一个矩形

JS 中实例化 fabric ,之后就可以使用 fabricapi 管理 canvas 了。

<script setup>
import { onMounted } from 'vue'
import { fabric } from 'fabric' // 引入 fabric

function init() {
  const canvas = new fabric.Canvas('c') // 这里传入的是canvas的id

  // 创建一个长方形
  const rect = new fabric.Rect({
    top: 30, // 距离容器顶部 30px
    left: 30, // 距离容器左侧 30px
    width: 100, // 宽 100px
    height: 60, // 高 60px
    fill: 'red' // 填充 红色
  })

  // 在canvas画布中加入矩形(rect)。
  canvas.add(rect)
}

onMounted(() => {
  init()
})
</script>

004.png

画布

Fabric.js 的画布操作性是非常强的。

基础版(可交互)

005.gif

基础版就是“起步”章节所说的那个例子。

<template>
  <canvas width="400" height="400" id="canvas" style="border: 1px solid #ccc;"></canvas>
</template>

<script setup>
import { onMounted } from 'vue'
import { fabric } from 'fabric'

function init() {
  const canvas = new fabric.Canvas('canvas') // 这里传入的是canvas元素的id

  // 创建一个长方形
  const rect = new fabric.Rect({
    top: 100, // 距离容器顶部 100px
    left: 100, // 距离容器左侧 100px
    width: 30, // 矩形宽度 30px
    height: 30, // 矩形高度 30px
    fill: 'red' // 填充 红色
  })

  canvas.add(rect) // 将矩形添加到 canvas 画布里
}

onMounted(() => {
  init()
})
</script>

不可交互

006.gif

<template>
  <canvas width="400" height="400" id="canvas" style="border: 1px solid #ccc;"></canvas>
</template>

<script setup>
import { onMounted } from 'vue'
import { fabric } from 'fabric'

function init() {
  // 使用 StaticCanvas 创建一个不可操作的画布
  const canvas = new fabric.StaticCanvas('canvas') // 这里传入的是canvas元素的id

  // 创建一个长方形
  const rect = new fabric.Rect({
    top: 100, // 距离容器顶部 100px
    left: 100, // 距离容器左侧 100px
    width: 30, // 矩形宽度 30px
    height: 30, // 矩形高度 30px
    fill: 'red' // 填充 红色
  })

  canvas.add(rect)
}

onMounted(() => {
  init()
})
</script>

创建不可交互的画布,其实只需把 new fabric.Canvas 改成 new fabric.StaticCanvas 即可。

在js设定画布参数

007.png

const canvas = new fabric.Canvas('canvas', {
    width: 300, // 画布宽度
    height: 300, // 画布高度
    backgroundColor: '#eee' // 画布背景色
})
</script>

使用背景图

<template>
  <canvas width="400" height="400" id="canvas" style="border: 1px solid #ccc;"></canvas>
</template>

<script setup>
import { onMounted } from 'vue'
import { fabric } from 'fabric'

function init() {
  const canvas = new fabric.Canvas('canvas')

  // 设置背景图
  // 参数1:背景图资源(可以引入本地,也可以使用网络图)
  // 参数2:设置完背景图执行以下重新渲染canvas的操作,这样背景图就会展示出来了
  canvas.setBackgroundImage(
    '图片url',
    canvas.renderAll.bind(canvas)
  )
}

onMounted(() => {
  init()
})
</script>

拉伸背景图

<template>
  <canvas width="400" height="400" id="canvas" style="border: 1px solid #ccc;"></canvas>
</template>

<script setup>
import { onMounted } from 'vue'
import { fabric } from 'fabric'

function init() {
  const canvas = new fabric.Canvas('canvas')

  // fabric.Image.fromURL:加载图片的api
  // 第一个参数:图片地址(可以是本地的,也可以是网络图)
  // 第二个参数:图片加载的回调函数
  fabric.Image.fromURL(
    '图片url',
    (img) => {
      // 设置背景图
      canvas.setBackgroundImage(
        img,
        canvas.renderAll.bind(canvas),
        {
          scaleX: canvas.width / img.width, // 计算出图片要拉伸的宽度
          scaleY: canvas.height / img.height // 计算出图片要拉伸的高度
        }
      )
    }
  )
}

onMounted(() => {
  init()
})
</script>

这个例子使用了 fabric.Image.fromURL 这个 api 来加载图片,第一个参数是图片地址,第二个参数是回调函数。

拿到图片的参数和画布的宽高进行计算,从而使图片充满全屏。

基础图形

矩形

015.png

<template>
  <canvas width="400" height="375" id="canvas" style="border: 1px solid #ccc;"></canvas>
</template>

<script setup>
import { onMounted } from 'vue'
import { fabric } from 'fabric'

function init() {
  const canvas = new fabric.Canvas('canvas')

  const rect = new fabric.Rect({
    top: 100, // 距离容器顶部 100px
    left: 100, // 距离容器左侧 100px
    fill: 'orange', // 填充 橙色
    width: 100, // 宽度 100px
    height: 100 // 高度 100px
  })
  
  // 将矩形添加到画布中
  canvas.add(rect)
}

onMounted(() => {
  init()
})
</script>

使用 new fabric.Rect 创建 矩形

圆角矩形

016.png

<template>
  <canvas width="400" height="375" id="canvas" style="border: 1px solid #ccc;"></canvas>
</template>

<script setup>
import { onMounted } from 'vue'
import { fabric } from 'fabric'

function init() {
  const canvas = new fabric.Canvas('canvas')

  const rect = new fabric.Rect({
    top: 100, // 距离容器顶部 100px
    left: 100, // 距离容器左侧 100px
    fill: 'orange', // 填充 橙色
    width: 100, // 宽度 100px
    height: 100, // 高度 100px
    rx: 20, // x轴的半径
    ry: 20 // y轴的半径
  })
  
  // 将矩形添加到画布中
  canvas.add(rect)
}

onMounted(() => {
  init()
})
</script>

画圆角矩形,需要添加 rxry

圆形

017.png

<template>
  <canvas width="400" height="375" id="canvas" style="border: 1px solid #ccc;"></canvas>
</template>

<script setup>
import { onMounted } from 'vue'
import { fabric } from 'fabric'

function init() {
  const canvas = new fabric.Canvas('canvas')

  const circle = new fabric.Circle({
    top: 100,
    left: 100,
    radius: 50, // 圆的半径 50
    fill: 'green'
  })
  canvas.add(circle)
}

onMounted(() => {
  init()
})
</script>

使用 new fabric.Circle 创建圆形

圆形需要使用 radius 设置半径大小。

椭圆形

018.png

<template>
  <canvas width="400" height="375" id="canvas" style="border: 1px solid #ccc;"></canvas>
</template>

<script setup>
import { onMounted } from 'vue'
import { fabric } from 'fabric'

function init() {
  const canvas = new fabric.Canvas('canvas')

  const ellipse = new fabric.Ellipse({
    top: 20,
    left: 20,
    rx: 70,
    ry: 30,
    fill: 'hotpink'
  })
  canvas.add(ellipse)
}

onMounted(() => {
  init()
})
</script>

需要使用 new fabric.Ellipse 创建 椭圆

和圆形不同,椭圆不需要设置 radius ,但要设置 rxry

  • rx > ry :椭圆是横着的
  • rx < ry:椭圆是竖着的
  • rx = ry: 看上去就是个圆形

三角形

019.png

<template>
  <canvas width="400" height="375" id="canvas" style="border: 1px solid #ccc;"></canvas>
</template>

<script setup>
import { onMounted } from 'vue'
import { fabric } from 'fabric'

function init() {
  const canvas = new fabric.Canvas('canvas')

  const triangle = new fabric.Triangle({
    top: 100,
    left: 100,
    width: 80, // 底边长度
    height: 100, // 底边到对角的距离
    fill: 'blue'
  })
  canvas.add(triangle)
}

onMounted(() => {
  init()
})
</script>

使用 new fabric.Triangle 创建三角形,三角形是需要给定 “底和高” 的。

线段

020.png

<template>
  <canvas width="400" height="375" id="canvas" style="border: 1px solid #ccc;"></canvas>
</template>

<script setup>
import { onMounted } from 'vue'
import { fabric } from 'fabric'

function init() {
  const canvas = new fabric.Canvas('canvas')

  const line = new fabric.Line(
    [
      10, 10, // 起始点坐标
      200, 300 // 结束点坐标
    ],
    {
      stroke: 'red', // 笔触颜色
    }
  )
  canvas.add(line)
}

onMounted(() => {
  init()
})
</script>

使用 new fabric.Line 创建线段。

new fabric.Line 需要传入2个参数。

  • 第一个参数是 数组 ,数组需要传4个值,前2个值是起始坐标的x和y,后2个值是结束坐标的x和y

  • 第二个参数是 线段的样式,要设置线段的颜色,需要使用 stroke

折线

021.png

<template>
  <canvas width="400" height="375" id="canvas" style="border: 1px solid #ccc;"></canvas>
</template>

<script setup>
import { onMounted } from 'vue'
import { fabric } from 'fabric'

function init() {
  const canvas = new fabric.Canvas('canvas')

  const polyline = new fabric.Polyline([
    {x: 30, y: 30},
    {x: 150, y: 140},
    {x: 240, y: 150},
    {x: 100, y: 30}
  ], {
    fill: 'transparent', // 如果画折线,需要填充透明
    stroke: '#6639a6', // 线段颜色:紫色
    strokeWidth: 5 // 线段粗细 5
  })
  canvas.add(polyline)
}

onMounted(() => {
  init()
})
</script>

使用 new fabric.Polyline 创建线段

new fabric.Polyline 需要传入2个参数。

  • 第一个参数是数组,描述线段的每一个点
  • 第二个参数用来描述线段样式

需要注意的是, fill 设置成透明才会显示成线段,如果不设置,会默认填充黑色,如下图所示:

022.png

你也可以填充自己喜欢的颜色,new fabric.Polyline 是不会自动把 起始点结束点 自动闭合起来的。

多边形

023.png

<template>
  <canvas width="400" height="375" id="canvas" style="border: 1px solid #ccc;"></canvas>
</template>

<script setup>
import { onMounted } from 'vue'
import { fabric } from 'fabric'

function init() {
  const canvas = new fabric.Canvas('canvas')

  const polygon = new fabric.Polygon([
    {x: 30, y: 30},
    {x: 150, y: 140},
    {x: 240, y: 150},
    {x: 100, y: 30}
  ], {
    fill: '#ffd3b6', // 填充色
    stroke: '#6639a6', // 线段颜色:紫色
    strokeWidth: 5 // 线段粗细 5
  })
  canvas.add(polygon)
}

onMounted(() => {
  init()
})
</script>

使用 new fabric.Polygon 绘制多边形,用法和 new fabric.Polyline 差不多,但最大的不同点是 new fabric.Polygon 会自动把 起始点结束点 连接起来。

自定义图形

在Fabric.js中,几乎所有的2d图形直接或间接继承自 Object 类,那么如果我们不用其自带的2d图形,而是自建图形,要怎么应用 Fabric.js 中的方法呢?

Fabric.js 提供了 fabric.util.createClass 方法解决这个问题

一个自定义子类的结构:

      // 创建一个自定义子类
      const customClass = fabric.util.createClass(fabric.Object, {
        type: "customClass",
        initialize: function (options) {
          options || (options = {});
          this.callSuper("initialize", options);
          // 自定义属性
        },

        toObject: function () {
          return fabric.util.object.extend(this.callSuper("toObject"), {
            // 将自定义属性添加到序列化对象中
          });
        },

        _render: function (ctx) {
          this.callSuper("_render", ctx);
          // 自定义渲染逻辑
        },
      });

一个简单的自定义类主要要修改3个地方,分别是:

  1. initialize : 添加的自定义属性方法放这
  2. toObject: 将自定义属性添加到序列化对象中,方便canvas记录
  3. _render: 处理自定义渲染逻辑

此处举一个简单的例子,写一个自定义表格图形:

新增绘制网格图的方法 initMap:

      // 绘制表格图形
      function initTable(options, ctx) {
        const { gridNumX, gridNumY, width, height, fill, left, top } = options;
        ctx.save();
        ctx.translate(-width / 2, -height / 2)
        // 开始路径并绘制线条
        ctx.beginPath();
        // 设置线条样式
        ctx.lineWidth = 1;
        ctx.strokeStyle = fill;
        // 开始绘制横线
        for (let i = 0; i < gridNumY + 1; i++) {
          // 注意要算线的宽度,也就是后面那个+i
          ctx.moveTo(0, height / gridNumY * i);
          ctx.lineTo(width, height / gridNumY * i);
          ctx.stroke();
        }
        // 开始绘制竖线
        for (let i = 0; i < gridNumX + 1; i++) {
          ctx.moveTo(width / gridNumX * i, 0);
          ctx.lineTo(width / gridNumX * i, height);
          ctx.stroke();
        }
        ctx.restore();
      }

创建 Table 子类:

      // 创建一个自定义子类
      const Map = fabric.util.createClass(fabric.Object, {
        type: "Table",
        initialize: function (options) {
          options || (options = {});
          this.callSuper("initialize", options);
          this.set("gridNumX", options.gridNumX || "");
          this.set("gridNumY", options.gridNumY || "");
        },

        toObject: function () {
          return fabric.util.object.extend(this.callSuper("toObject"), {
            gridNumX: this.get("gridNumX"),
            gridNumY: this.get("gridNumY"),
          });
        },

        _render: function (ctx) {
          this.callSuper("_render", ctx);
          initTable({
            ...this
          }, ctx)
        },
      });

新建 Table 实例并添加到canvas:

      const table = new Table({
        left: 100,
        top: 100,
        label: "test",
        fill: "#faa",
        width: 100,
        height: 100,
        gridNumX: 4,
        gridNumY: 3
      });

      const table2 = new Table({
        left: 300,
        top: 100,
        label: "test",
        fill: "green",
        width: 200,
        height: 300,
        gridNumX: 2,
        gridNumY: 5
      });
      // 将所有图形添加到 canvas 中
      canvas.add(table, table2);

如图所示,成功创建了可复用的自定义图形,而且能够使用 Object 类的功能。

image.png

文本

Fabric.js 有3类跟文本相关的 api

  • 普通文本
  • 可编辑文本
  • 文本框

普通文本 Text

<template>
  <canvas width="400" height="400" id="canvas" style="border: 1px solid #ccc;"></canvas>
</template>

<script setup>
import { onMounted } from 'vue'
import { fabric } from 'fabric'

function init() {
  const canvas = new fabric.Canvas('canvas')

  const text = new fabric.Text('hello')
  canvas.add(text)
}

onMounted(() => {
  init()
})
</script>

使用 new fabric.Text 创建文本,传入第一个参数就是文本内容。

new fabric.Text 还支持第二个参数,可以设置文本样式。

可编辑文本 IText

<template>
  <canvas width="400" height="400" id="canvas" style="border: 1px solid #ccc;"></canvas>
</template>

<script setup>
import { onMounted } from 'vue'
import { fabric } from 'fabric'

function init() {
  const canvas = new fabric.Canvas('canvas')

  const itext = new fabric.IText('hello')
  canvas.add(itext)
}

onMounted(() => {
  init()
})
</script>

使用 new fabric.IText 可以创建可编辑文本,用法和 new fabric.Text 一样。

文本框 Textbox

<template>
  <canvas width="400" height="400" id="canvas" style="border: 1px solid #ccc;"></canvas>
</template>

<script setup>
import { onMounted } from 'vue'
import { fabric } from 'fabric'

function init() {
  const canvas = new fabric.Canvas('canvas')

  const textbox = new fabric.Textbox('What are you doing?', {
    width: 250
  })
  canvas.add(textbox)
}

onMounted(() => {
  init()
})
</script>

使用 new fabric.Textbox 可以创建文本框。

new fabric.Textbox 第二个参数是对象,使用 width 可以设定了文本框的宽度,文本内容超过设定的宽度会自动换行。

new fabric.Textbox 的内容同样是可编辑的。

基础样式

图形常用样式

<template>
  <canvas width="400" height="400" id="canvas" style="border: 1px solid #ccc;"></canvas>
</template>

<script setup>
import { onMounted } from 'vue'
import { fabric } from 'fabric'

function init() {
  const canvas = new fabric.Canvas('canvas')

  const circle = new fabric.Circle({
    top: 100,
    left: 100,
    radius: 50, // 半径:50px
    backgroundColor: 'green', // 背景色:绿色
    fill: 'orange', // 填充色:橙色
    stroke: '#f6416c', // 边框颜色:粉色
    strokeWidth: 5, // 边框粗细:5px
    strokeDashArray: [20, 5, 14], // 边框虚线规则:填充20px 空5px 填充14px 空20px 填充5px ……
    shadow: '10px 20px 6px rgba(10, 20, 30, 0.4)', // 投影:向右偏移10px,向下偏移20px,羽化6px,投影颜色及透明度
    transparentCorners: false, // 选中时,角是被填充了。true 空心;false 实心
    borderColor: '#16f1fc', // 选中时,边框颜色:天蓝
    borderScaleFactor: 5, // 选中时,边的粗细:5px
    borderDashArray: [20, 5, 10, 7], // 选中时,虚线边的规则
    cornerColor: "#a1de93", // 选中时,角的颜色是 青色
    cornerStrokeColor: 'pink', // 选中时,角的边框的颜色是 粉色
    cornerStyle: 'circle', // 选中时,叫的属性。默认rect 矩形;circle 圆形
    cornerSize: 20, // 选中时,角的大小为20
    cornerDashArray: [10, 2, 6], // 选中时,虚线角的规则
    selectionBackgroundColor: '#7f1300', // 选中时,选框的背景色:朱红
    padding: 40, // 选中时,选择框离元素的内边距:40px
    borderOpacityWhenMoving: 0.6, // 当对象活动和移动时,对象控制边界的不透明度  
  })

  canvas.add(circle)
}

onMounted(() => {
  init()
})
</script>

文本常用样式

<template>
  <canvas width="400" height="400" id="canvas" style="border: 1px solid #ccc;"></canvas>
</template>

<script setup>
import { onMounted } from 'vue'
import { fabric } from 'fabric'

function init() {
  const canvas = new fabric.Canvas('canvas')

  const text = new fabric.Text('hello', {
    top: 40,
    left: 40,
    fontSize: 120,
    backgroundColor: 'green', // 背景色:绿色
    fill: 'orange', // 填充色:橙色
    stroke: '#f6416c', // 边框颜色:粉色
    strokeWidth: 3, // 边框粗细:3px
    strokeDashArray: [20, 5, 14], // 边框虚线规则:填充20px 空5px 填充14px 空20px 填充5px ……
    shadow: '10px 20px 6px rgba(10, 20, 30, 0.4)', // 投影:向右偏移10px,向下偏移20px,羽化6px,投影颜色及透明度
    transparentCorners: false, // 选中时,角是被填充了。true 空心;false 实心
    borderColor: '#16f1fc', // 选中时,边框颜色:天蓝
    borderScaleFactor: 5, // 选中时,边的粗细:5px
    borderDashArray: [20, 5, 10, 7], // 选中时,虚线边的规则
    cornerColor: "#a1de93", // 选中时,角的颜色是 青色
    cornerStrokeColor: 'pink', // 选中时,角的边框的颜色是 粉色
    cornerStyle: 'circle', // 选中时,叫的属性。默认rect 矩形;circle 圆形
    cornerSize: 20, // 选中时,角的大小为20
    cornerDashArray: [10, 2, 6], // 选中时,虚线角的规则
    selectionBackgroundColor: '#7f1300', // 选中时,选框的背景色:朱红
    padding: 40, // 选中时,选择框离元素的内边距:40px
    borderOpacityWhenMoving: 0.6, // 当对象活动和移动时,对象控制边界的不透明度  
  })

  canvas.add(text)
}

onMounted(() => {
  init()
})
</script>

除此之外,还可以配置 上划线下划线删除线左对齐右对齐居中对齐行距 等。

030.png

<template>
  <canvas width="600" height="400" id="canvas" style="border: 1px solid #ccc;"></canvas>
</template>

<script setup>
import { onMounted } from 'vue'
import { fabric } from 'fabric'

function init() {
  const canvas = new fabric.Canvas('canvas')

  // 上划线
  const overline = new fabric.Text('上划线', {
    top: 30,
    left: 10,
    fontSize: 20,
    overline: true, // 上划线
  })

  // 下划线
  const underline = new fabric.Text('下划线', {
    top: 30,
    left: 100,
    fontSize: 20,
    underline: true, // 下划线
  })

  // 删除线
  const linethrough = new fabric.Text('删除线', {
    top: 30,
    left: 200,
    fontSize: 20,
    linethrough: true, // 删除线
  })

  // 左对齐
  const msg1 = '左\n左左\n左对齐'
  const left = new fabric.Text(msg1, {
    top: 100,
    left: 10,
    fontSize: 16,
    textAlign: 'left', // 左对齐
  })

  // 居中对齐
  const msg2 = '中\n中中\n居中对齐'
  const center = new fabric.Text(msg2, {
    top: 100,
    left: 100,
    fontSize: 16,
    textAlign: 'center',// 居中对齐
  })

  // 右对齐
  const msg3 = '右\n右右\n右对齐'
  const right = new fabric.Text(msg3, {
    top: 100,
    left: 200,
    fontSize: 16,
    textAlign: 'right', // 右对齐
  })

  // 文本内容
  const msg4 = "What are you doing,\nWhat are you doing,\nWhat are you doing\What are you doing"
  
  const lineHeight1 = new fabric.Text(msg4, {
    top: 250,
    left: 10,
    fontSize: 16,
    lineHeight: 1, // 行高
  })

  const lineHeight2 = new fabric.Text(msg4, {
    top: 250,
    left: 300,
    fontSize: 16,
    lineHeight: 2, // 行高
  })

  canvas.add(
    overline,
    underline,
    linethrough,
    left,
    center,
    right,
    lineHeight1,
    lineHeight2
  )

}

onMounted(() => {
  init()
})
</script>

渐变

线性渐变

031.png

<template>
  <canvas width="600" height="600" id="canvas" style="border: 1px solid #ccc;"></canvas>
</template>

<script setup>
import { onMounted } from 'vue'
import { fabric } from 'fabric'

function init() {
  let canvas = new fabric.Canvas('canvas')

  // 圆
  let circle = new fabric.Circle({
    left: 100,
    top: 100,
    radius: 50,
  })

  // 线性渐变
  let gradient = new fabric.Gradient({
    type: 'linear', // linear or radial
    gradientUnits: 'pixels', // pixels or pencentage 像素 或者 百分比
    coords: { x1: 0, y1: 0, x2: circle.width, y2: 0 }, // 至少2个坐标对(x1,y1和x2,y2)将定义渐变在对象上的扩展方式
    colorStops:[ // 定义渐变颜色的数组
      { offset: 0, color: 'red' },
      { offset: 0.2, color: 'orange' },
      { offset: 0.4, color: 'yellow' },
      { offset: 0.6, color: 'green' },
      { offset: 0.8, color: 'blue' },
      { offset: 1, color: 'purple' },
    ]
  })
  circle.set('fill', gradient);
  canvas.add(circle)
}

onMounted(() => {
  init()
})
</script>

径向渐变

032.png

<template>
  <canvas width="600" height="600" id="canvas" style="border: 1px solid #ccc;"></canvas>
</template>

<script setup>
import { onMounted } from 'vue'
import { fabric } from 'fabric'

function init() {
  let canvas = new fabric.Canvas('canvas')

  // 圆
  let circle = new fabric.Circle({
    left: 100,
    top: 100,
    radius: 50,
  })

  let gradient = new fabric.Gradient({
    type: 'radial',
    coords: {
      r1: 50, // 该属性仅径向渐变可用,外圆半径
      r2: 0, // 该属性仅径向渐变可用,外圆半径  
      x1: 50, // 焦点的x坐标
      y1: 50, // 焦点的y坐标
      x2: 50, // 中心点的x坐标
      y2: 50, // 中心点的y坐标
    },
    colorStops: [
      { offset: 0, color: '#fee140' },
      { offset: 1, color: '#fa709a' }
    ]
  })

  circle.set('fill', gradient);
  canvas.add(circle)
}

onMounted(() => {
  init()
})
</script>

选中状态

Fabric.js 创建出来的元素(图形、图片、组等)默认是可以被选中的。

禁止选中

055.gif

<template>
  <canvas width="400" height="400" id="canvas" style="border: 1px solid #ccc;"></canvas>
</template>

<script setup>
import { onMounted } from 'vue'
import { fabric } from 'fabric'

function init() {
  const canvas = new fabric.Canvas('canvas')

  const rect = new fabric.Rect({
    top: 100,
    left: 100,
    width: 200,
    height: 100,
    fill: 'red'
  })

  // 元素禁止选中
  rect.selectable = false

  canvas.add(rect)
}

onMounted(() => {
  init()
})
</script>

框选样式

<template>
  <canvas width="200" height="200" id="canvas" style="border: 1px solid #ccc;"></canvas>
</template>

<script setup>
import { onMounted } from 'vue'
import { fabric } from 'fabric'

function init() {
  const canvas = new fabric.Canvas('canvas')

  // 圆形
  const circle = new fabric.Circle({
    radius: 30,
    fill: '#f55',
    top: 70,
    left: 70
  })

  canvas.add(circle)

  canvas.selection = true // 画布是否可选中。默认true;false 不可选中
  canvas.selectionColor = 'rgba(106, 101, 216, 0.3)' // 画布鼠标框选时的背景色
  canvas.selectionBorderColor = "#1d2786" // 画布鼠标框选时的边框颜色
  canvas.selectionLineWidth = 6 // 画布鼠标框选时的边框厚度
  canvas.selectionDashArray = [30, 4, 10] // 画布鼠标框选时边框虚线规则
  canvas.selectionFullyContained = true // 只选择完全包含在拖动选择矩形中的形状
}

onMounted(() => {
  init()
})
</script>

自定义边和控制角样式

058.png

<template>
  <canvas width="200" height="200" id="canvas" style="border: 1px solid #ccc;"></canvas>
</template>

<script setup>
import { onMounted } from 'vue'
import { fabric } from 'fabric'

function init() {
  const canvas = new fabric.Canvas('canvas')

  // 圆形
  const circle = new fabric.Circle({
    radius: 30,
    fill: '#f55',
    top: 70,
    left: 70
  })

  circle.set({
    borderColor: 'red', // 边框颜色
    cornerColor: 'green', // 控制角颜色
    cornerSize: 10, // 控制角大小
    transparentCorners: false // 控制角填充色不透明
  })

  canvas.add(circle)

  canvas.setActiveObject(circle) // 选中圆
}

onMounted(() => {
  init()
})
</script>

没有控制角

没有控制角将意味着无法用鼠标直接操作缩放和旋转,只允许移动操作。

062.png

<template>
  <canvas width="200" height="200" id="canvas" style="border: 1px solid #ccc;"></canvas>
</template>

<script setup>
import { onMounted } from 'vue'
import { fabric } from 'fabric'

function init() {
  const canvas = new fabric.Canvas('canvas')

  // 圆形
  const circle = new fabric.Circle({
    radius: 30,
    fill: '#f55',
    top: 70,
    left: 70
  })

  circle.hasControls = false // 禁止控制角

  canvas.add(circle)

  canvas.setActiveObject(circle) // 选中第一项
}

onMounted(() => {
  init()
})
</script>

不允许框选

不允许从画布框选,但允许选中元素。

065.gif

<template>
  <canvas width="200" height="200" id="canvas" style="border: 1px solid #ccc;"></canvas>
</template>

<script setup>
import { onMounted } from 'vue'
import { fabric } from 'fabric'

function init() {
  const canvas = new fabric.Canvas('canvas')

  // 圆形
  const circle = new fabric.Circle({
    radius: 30,
    fill: '#f55',
    top: 70,
    left: 70
  })

  canvas.add(circle)
  canvas.selection = false // 不允许直接从画布框选
}

onMounted(() => {
  init()
})
</script>

分组

建组

039.gif

<template>
  <div>
    <canvas width="400" height="400" id="canvas" style="border: 1px solid #ccc;"></canvas>
  </div>
</template>

<script setup>
import { onMounted } from 'vue'
import { fabric } from 'fabric'

function init() {
  const canvas = new fabric.Canvas('canvas')

  // 椭圆
  const ellipse = new fabric.Ellipse({
    top: 20,
    left: 20,
    rx: 100,
    ry: 50,
    fill: '#ddd',
    originX: 'center', // 旋转x轴:left, right, center
    originY: 'center' // 旋转y轴:top, bottom, center
  })

  // 文本
  const text = new fabric.Text('Hello World', {
    top: 40,
    left: 20,
    fontSize: 20,
    originX: "center",
    originY: "center"
  })

  // 建组
  const group = new fabric.Group([ellipse, text], {
    top: 50, // 整组距离顶部100
    left: 100, // 整组距离左侧100
    angle: -10, // 整组旋转-10deg
  })

  canvas.add(group)
}

onMounted(() => {
  init()
})
</script>

new fabric.Group 可以创建一个组,把多个图层放在同一个组内,实现同步的操作,比如拖拽、缩放等。

操作组

Fabric.js 的组提供了很多方法,这里列一些常用的:

  • getObjects() 返回一组中所有对象的数组

  • size() 所有对象的数量

  • contains() 检查特定对象是否在 group

  • item() 组中元素

  • forEachObject() 遍历组中对象

  • add() 添加元素对象

  • remove() 删除元素对象

  • fabric.util.object.clone() 克隆

<template>
  <div>
    <canvas width="400" height="400" id="canvas" style="border: 1px solid #ccc;"></canvas>
  </div>
</template>

<script setup>
import { onMounted } from 'vue'
import { fabric } from 'fabric'

function init() {
  const canvas = new fabric.Canvas('canvas')

  // 椭圆
  const ellipse = new fabric.Ellipse({
    top: 20,
    left: 20,
    rx: 100,
    ry: 50,
    fill: '#ddd',
    originX: 'center', // 旋转x轴:left, right, center
    originY: 'center' // 旋转y轴:top, bottom, center
  })

  // 文本
  const text = new fabric.Text('Hello World', {
    top: 40,
    left: 20,
    fontSize: 20,
    originX: "center",
    originY: "center"
  })

  // 建组
  const group = new fabric.Group([ellipse, text], {
    top: 50, // 整组距离顶部100
    left: 100, // 整组距离左侧100
    angle: -10, // 整组旋转-10deg
  })

  // 控制第一个元素(椭圆)
  group.item(0).set('fill', '#ea5455')

  // 控制第二个元素(文本)
  group.item(1).set({
    text: '雷猴,世界',
    fill: '#fff'
  })

  canvas.add(group)
}

onMounted(() => {
  init()
})
</script>

取消分组

041.gif

<template>
  <div>
    <button @click="ungroup">取消组</button>
    <canvas width="400" height="400" id="canvas" style="border: 1px solid #ccc;"></canvas>
  </div>
</template>

<script setup>
import { onMounted } from 'vue'
import { fabric } from 'fabric'

let canvas = null

// 初始化
function init() {
  canvas = new fabric.Canvas('canvas')

  // 椭圆
  const ellipse = new fabric.Ellipse({
    top: 20,
    left: 20,
    rx: 100,
    ry: 50,
    fill: '#ddd',
    originX: 'center', // 旋转x轴:left, right, center
    originY: 'center' // 旋转y轴:top, bottom, center
  })

  // 文本
  const text = new fabric.Text('Hello World', {
    top: 40,
    left: 20,
    fontSize: 20,
    originX: "center",
    originY: "center"
  })

  // 建组
  const group = new fabric.Group([ellipse, text], {
    top: 50, // 整组距离顶部100
    left: 100, // 整组距离左侧100
    angle: -10, // 整组旋转-10deg
  })

  canvas.add(group)
}

// 取消组
function ungroup() {
  // 判断当前有没有选中元素,如果没有就不执行任何操作
  if (!canvas.getActiveObject()) {
    return
  }

  // 判断当前是否选中组,如果不是,就不执行任何操作
  if (canvas.getActiveObject().type !== 'group') {
    return
  }

  // 先获取当前选中的对象,然后打散
  canvas.getActiveObject().toActiveSelection()
}

onMounted(() => {
  init()
})
</script>

动画

绝对值动画

042.gif

<template>
  <div>
    <canvas width="400" height="400" id="canvas" style="border: 1px solid #ccc;"></canvas>
  </div>
</template>

<script setup>
import { onMounted } from 'vue'
import { fabric } from 'fabric'

// 初始化
function init() {
  const canvas = new fabric.Canvas('canvas')

  const rect = new fabric.Rect({
    left: 100,
    top: 100,
    width: 100,
    height: 100,
    fill: 'red'
  })

  // 设置矩形动画
  rect.animate('angle', "-50", {
    onChange:canvas.renderAll.bind(canvas), // 每次刷新的时候都会执行
  })

  canvas.add(rect)
}

onMounted(() => {
  init()
})
</script>

每个 Fabric 对象都有一个 animate 方法,该方法可以动画化该对象。

用法:animate(动画属性, 动画的结束值, [画的详细信息])

第一个参数是要设置动画的属性。

第二个参数是动画的结束值。

第三个参数是一个对象,包括:

{

   rom:允许指定可设置动画的属性的起始值(如果我们不希望使用当前值)。

   duration:默认为500(ms)。可用于更改动画的持续时间。

   onComplete:在动画结束时调用的回调。

   easing:缓动功能。

}

相对值动画

043.gif

<template>
  <div>
    <canvas width="400" height="400" id="canvas" style="border: 1px solid #ccc;"></canvas>
  </div>
</template>

<script setup>
import { onMounted } from 'vue'
import { fabric } from 'fabric'

// 初始化
function init() {
  const canvas = new fabric.Canvas('canvas')
  const rect = new fabric.Rect({
    left: 100,
    top: 100,
    width: 100,
    height: 100,
    fill: 'red'
  })

  // 请注意第二个参数:+=360
  rect.animate('angle', '+=360', {
    onChange:canvas.renderAll.bind(canvas), // 每次刷新的时候都会执行
    duration: 2000, // 执行时间
    easing: fabric.util.ease.easeOutBounce, // 缓冲效果
  })

  canvas.add(rect)
}

onMounted(() => {
  init()
})
</script>

绝对值动画相对值动画 的用法是差不多的,只是 第二个参数 用法不同。

相对值动画 是把 animate 改成带上运算符的值,这样就会在原基础上做计算。

设置图形层级

  • Canvas对象层级操作方法

    • canvas.bringToFront(object): 将指定对象移到最前面。
    • canvas.sendToBack(object): 将指定对象移到最后面。
    • canvas.bringForward(object): 将指定对象向前移动一个层级。
    • canvas.sendBackwards(object): 将指定对象向后移动一个层级。
    • canvas.moveTo(object, index): 将指定对象移动到指定的层级索引。
  • Object对象层级操作方法

    • object.bringToFront(): 将当前对象移到最前面。
    • object.sendToBack(): 将当前对象移到最后面。
    • object.bringForward(intersecting): 将当前对象向前移动一个层级,若intersecting为true则会跳过所有交叉的对象。
    • object.sendBackwards(intersecting): 将当前对象向后移动一个层级,若intersecting为true则会跳过所有交叉的对象。
    • object.moveTo(index): 将当前对象移动到指定的层级索引。

想要获取具体图形的层级一般使用 canvas.getObjects().indexOf(xxx)

显然,这个有点麻烦,我们自己加一个 level 方法让其直接显示对象的层级。

// 新增 level 方法
fabric.Object.prototype.getLevel = function() {
  return this.canvas.getObjects().indexOf(this);
}
// 添加到画布
canvas.add(rect, circle, triangle);
// 调用level方法
console.log(rect.getLevel()); // 0
console.log(triangle.getLevel()); // 1

事件

Fabric.js 提供了一套很方便的事件系统,可以用 on 方法可以初始化事件监听器,用 off 方法将其删除。

045.gif

<template>
  <div>
    <canvas width="400" height="400" id="canvas" style="border: 1px solid #ccc;"></canvas>
    <button @click="addClickEvent">添加画布点击事件</button>
    <button @click="removeClickEvent">移除画布点击事件</button>
  </div>
</template>

<script setup>
import { onMounted } from 'vue'
import { fabric } from 'fabric'

let canvas = null

// 初始化画布
function init() {
  canvas = new fabric.Canvas('canvas')

  const rect = new fabric.Rect({
    top: 20,
    left: 20,
    width: 100,
    height: 50,
    fill: '#9896f1'
  })

  // 给矩形添加一个选中事件
  rect.on('selected', options => {
    console.log('选中矩形啦', options)
  })
  canvas.add(rect)

  addClickEvent()
}

// 移除画布点击事件
function removeClickEvent() {
  canvas.off('mouse:down')
}

// 添加画布点击事件
function addClickEvent() {
  removeClickEvent() // 在添加事件之前先把该事件清除掉,以免重复添加
  canvas.on('mouse:down', options => {
    console.log(`x轴坐标: ${options.e.clientX};    y轴坐标: ${options.e.clientY}`)
  })
}

onMounted(() => {
  init()
})
</script>

禁止操作框的部分功能

禁止水平移动

047.gif

<template>
  <div>
    <canvas id="canvas" width="400" height="400" style="border: 1px solid #ccc;"></canvas>
  </div>
</template>

<script setup>
import { onMounted } from 'vue'
import { fabric } from 'fabric'

// 初始化画布
function init() {
  const canvas = new fabric.Canvas('canvas')

  const rect = new fabric.Rect({
    top: 100,
    left: 100,
    width: 100,
    height: 50,
    fill: '#ffde7d'
  })

  // 不允许水平移动
  rect.lockMovementX = true

  canvas.add(rect)
}

onMounted(() => {
  init()
})
</script>

禁止垂直移动

048.gif

<template>
  <div>
    <canvas id="canvas" width="400" height="400" style="border: 1px solid #ccc;"></canvas>
  </div>
</template>

<script setup>
import { onMounted } from 'vue'
import { fabric } from 'fabric'

// 初始化画布
function init() {
  const canvas = new fabric.Canvas('canvas')

  const rect = new fabric.Rect({
    top: 100,
    left: 100,
    width: 100,
    height: 50,
    fill: '#f6416c'
  })

  // 不允许垂直移动
  rect.lockMovementY = true

  canvas.add(rect)
}

onMounted(() => {
  init()
})
</script>

缩放和平移画布

缩放画布

以原点为基准缩放画布

需要监听鼠标的滚轮事件:mouse:wheel

052.gif

<template>
  <div>
    <canvas id="canvas" width="400" height="400" style="border: 1px solid #ccc;"></canvas>
  </div>
</template>

<script setup>
import { onMounted } from 'vue'
import { fabric } from 'fabric'

function init() {
  // 初始化画布
  const canvas = new fabric.Canvas('canvas')

  // 矩形
  const rect = new fabric.Rect({
    top: 10,
    left: 10,
    width: 40,
    height: 40,
    fill: 'orange'
  })

  // 圆形
  const circle = new fabric.Circle({
    top: 30,
    left: 30,
    radius: 50,
    fill: 'green'
  })
  canvas.add(rect, circle) // 将矩形和圆形添加到画布中

  // 监听鼠标滚轮事件
  canvas.on('mouse:wheel', opt => {
    let delta = opt.e.deltaY // 滚轮向上滚一下是 -100,向下滚一下是 100
    let zoom = canvas.getZoom() // 获取画布当前缩放值

    // 控制缩放范围在 0.01~20 的区间内
    zoom *= 0.999 ** delta
    if (zoom > 20) zoom = 20
    if (zoom < 0.01) zoom = 0.01

    // 设置画布缩放比例
    canvas.setZoom(zoom)
  })
}

onMounted(() => {
  init()
})
</script>

以鼠标指针为基准缩放画布

053.gif

<template>
  <div>
    <canvas id="canvas" width="400" height="400" style="border: 1px solid #ccc;"></canvas>
  </div>
</template>

<script setup>
import { onMounted } from 'vue'
import { fabric } from 'fabric'

function init() {
  // 初始化画布
  const canvas = new fabric.Canvas('canvas')

  // 矩形
  const rect = new fabric.Rect({
    top: 130,
    left: 130,
    width: 40,
    height: 40,
    fill: 'orange'
  })

  // 圆形
  const circle = new fabric.Circle({
    top: 150,
    left: 150,
    radius: 50,
    fill: 'green'
  })
  canvas.add(rect, circle) // 将矩形和圆形添加到画布中

  // 监听鼠标滚轮事件
  canvas.on('mouse:wheel', opt => {
    let delta = opt.e.deltaY // 滚轮向上滚一下是 -100,向下滚一下是 100
    let zoom = canvas.getZoom() // 获取画布当前缩放值

    // 控制缩放范围在 0.01~20 的区间内
    zoom *= 0.999 ** delta
    if (zoom > 20) zoom = 20
    if (zoom < 0.01) zoom = 0.01

    // 设置画布缩放比例
    // 关键点!!!
    // 参数1:将画布的所放点设置成鼠标当前位置
    // 参数2:传入缩放值
    canvas.zoomToPoint(
      {
        x: opt.e.offsetX, // 鼠标x轴坐标
        y: opt.e.offsetY  // 鼠标y轴坐标
      },
      zoom // 最后要缩放的值
    )
  })
}

onMounted(() => {
  init()
})
</script>

平移画布

<template>
  <div>
    <canvas id="canvas" width="400" height="400" style="border: 1px solid #ccc;"></canvas>
  </div>
</template>

<script setup>
import { onMounted } from 'vue'
import { fabric } from 'fabric'

function init() {
  // 初始化画布
  const canvas = new fabric.Canvas('canvas')

  // 矩形
  const rect = new fabric.Rect({
    top: 130,
    left: 130,
    width: 40,
    height: 40,
    fill: 'orange'
  })

  // 圆形
  const circle = new fabric.Circle({
    top: 150,
    left: 150,
    radius: 50,
    fill: 'green'
  })
  canvas.add(rect, circle) // 将矩形和圆形添加到画布中

  canvas.on('mouse:down', opt => { // 鼠标按下时触发
    let evt = opt.e
    if (evt.altKey === true) { // 是否按住alt
      canvas.isDragging = true // isDragging 是自定义的,开启移动状态
      canvas.lastPosX = evt.clientX // lastPosX 是自定义的
      canvas.lastPosY = evt.clientY // lastPosY 是自定义的
    }
  })

  canvas.on('mouse:move', opt => { // 鼠标移动时触发
    if (canvas.isDragging) {
      let evt = opt.e
      let vpt = canvas.viewportTransform // 聚焦视图的转换
      vpt[4] += evt.clientX - canvas.lastPosX
      vpt[5] += evt.clientY - canvas.lastPosY
      canvas.requestRenderAll() // 重新渲染
      canvas.lastPosX  = evt.clientX
      canvas.lastPosY  = evt.clientY
    }
  })

  canvas.on('mouse:up', opt => { // 鼠标松开时触发
    canvas.setViewportTransform(canvas.viewportTransform) // 设置此画布实例的视口转换  
    canvas.isDragging = false // 关闭移动状态
  })
}

onMounted(() => {
  init()
})
</script>

获取真实的转换坐标

在图像处理的过程中,我们经常会用到坐标点信息,以便于进行一些交互操作。

此处举一个简单的例子,当鼠标点击时,在鼠标的位置创建一个方块对象:

      // 当鼠标按下时
      canvas.on('mouse:down', function(option) {
        const evt = option.e;
        // 创建一个小方块
        this.add(new fabric.Rect({
          left: evt.offsetX,
          top: evt.offsetY,
          width: 50,
          height: 50,
          fill: 'yellow'
        }))
        this.renderAll();
      });

recording.gif

从上图可见,当canvas未平移或缩放时,可以很简单的获取相应点位置,但是一但平移或者缩放后,鼠标点的位置就全乱了。Fabric.js 提供了 transformPoint 方法解决这一问题。

  • fabric.util.transformPoint(Point, transform) :

    • 将Canvas坐标点转换为视口坐标点
    • 例如:fabric.util.transformPoint(new fabric.Point(100, 100), canvas.viewportTransform) ,将视口的(100,100)坐标点转化为平移缩放后的坐标点。
  • Canvas.getPointer(event) :

    • 用于获取事件(如鼠标或触摸事件)发生时相对于画布的坐标。它考虑了当前视口的变换(包括平移和缩放),因此可以正确地将鼠标或触摸事件的屏幕坐标转换为画布坐标。

修改代码:

      // 当鼠标按下时
      canvas.on('mouse:down', function(option) {
        const evt = option.e;
        // 用transformPoint创建一个小方块
        // 注意 transformPoint 作用是将一个坐标从一个坐标系转换到另一个坐标系
        // 由于这里的将按下的视口坐标转换成 canvas画布坐标系,所以需要用 invertTransform 反转变换
        this.add(new fabric.Rect({
          left: fabric.util.transformPoint({ x: evt.offsetX, y: evt.offsetY },  fabric.util.invertTransform(canvas.viewportTransform)).x,
          top: fabric.util.transformPoint({ x: evt.offsetX, y: evt.offsetY }, fabric.util.invertTransform(canvas.viewportTransform)).y,
          width: 50,
          height: 50,
          fill: 'red'
        }))
        // 用getPointer创建一个小方块
        const pointer = canvas.getPointer(evt);
        console.log('potint, ', pointer)
        this.add(new fabric.Rect({
          left: pointer.x,
          top: pointer.y,
          width: 50,
          height: 50,
          fill: 'blue'
        }))
        this.renderAll();
      });

注意 transformPoint 作用是将一个坐标从一个坐标系转换到另一个坐标系,由于这里的将按下的视口坐标转换成 canvas画布坐标系,所以需要用 invertTransform 反转变换。

recording.gif

序列化

输出JSON

<template>
  <div>
    <canvas id="canvas" width="200" height="200" style="border: 1px solid #ccc;"></canvas>
  </div>
</template>

<script setup>
import { onMounted } from 'vue'
import { fabric } from 'fabric'

function init() {
  // 初始化画布
  const canvas = new fabric.Canvas('canvas')

  console.log('canvas stringify ', JSON.stringify(canvas))
  console.log('canvas toJSON', canvas.toJSON())
  console.log('canvas toObject', canvas.toObject())
}

onMounted(() => {
  init()
})
</script>

打开控制台可以看到输出。

输出base64

<template>
  <div>
    <canvas id="canvas" width="200" height="200" style="border: 1px solid #ccc;"></canvas>
  </div>
</template>

<script setup>
import { onMounted } from 'vue'
import { fabric } from 'fabric'

function init() {
  // 初始化画布
  const canvas = new fabric.Canvas('canvas', {
    backgroundColor: '#a5dee5'
  })

  const rect = new fabric.Rect({
    left: 50,
    top: 50,
    height: 20,
    width: 20,
    fill: 'green'
  })

  const circle = new fabric.Circle({
    left: 80,
    top: 80,
    radius: 40,
    fill: "red"
  })

  canvas.add(rect, circle)

  console.log('toPng', canvas.toDataURL('png')) // 在控制台输出 png(base64)
  canvas.requestRenderAll()
}

onMounted(() => {
  init()
})
</script>

输出 SVG

<template>
  <div>
    <canvas id="canvas" width="200" height="200" style="border: 1px solid #ccc;"></canvas>
  </div>
</template>

<script setup>
import { onMounted } from 'vue'
import { fabric } from 'fabric'

function init() {
  // 初始化画布
  const canvas = new fabric.Canvas('canvas', {
    backgroundColor: '#a5dee5'
  })

  const rect = new fabric.Rect({
    left: 50,
    top: 50,
    height: 20,
    width: 20,
    fill: 'green'
  })

  const circle = new fabric.Circle({
    left: 80,
    top: 80,
    radius: 40,
    fill: "red"
  })

  canvas.add(rect, circle)

  console.log(canvas.toSVG()) // 输出 SVG
}

onMounted(() => {
  init()
})
</script>

反序列化

反序列化就是把 JSON 数据渲染到画布上。

通常把从后台请求回来的数据渲染到画布上。

<template>
  <div>
    <canvas id="canvas" width="200" height="200" style="border: 1px solid #ccc;"></canvas>
  </div>
</template>

<script setup>
import { onMounted } from 'vue'
import { fabric } from 'fabric'

function init() {
  const jsonStr = ''

  // 初始化画布
  const canvas = new fabric.Canvas('canvas')

  // 反序列化
  canvas.loadFromJSON(jsonStr)
}

onMounted(() => {
  init()
})
</script>

总结

写到这里,我们已经把 Fabric.js 常见功能都过了一遍。如果你坚持看到这里,恭喜你!你已经具备构建一个简单画板/海报编辑器/可视化工具的基础能力。

Fabric.js 是一个非常强大的前端 Canvas 库,随着你不断实践,你会发现它能做的事情远不止本文展示的这些。

如果你有什么想法、问题或希望我继续写的方向,欢迎在评论区告诉我,我会持续更新更多相关内容。

Vue v-for 遍历对象顺序完全指南:从混乱到可控

Vue v-for 遍历对象顺序完全指南:从混乱到可控

Vue 中 v-for 遍历对象的顺序问题经常让开发者困惑。今天我们来彻底搞懂它的遍历机制,并掌握多种保证顺序的方法!

一、问题的核心:JavaScript 对象顺序的真相

1.1 JavaScript 对象的无序性

// 实验1:JavaScript 原生对象
const obj = {
  3: 'three',
  1: 'one',
  2: 'two'
}

console.log(Object.keys(obj))  // 输出什么?
// 结果是:['1', '2', '3']!数字键被排序了!

// 实验2:混合键名
const mixedObj = {
  c: 'Charlie',
  a: 'Alpha',
  2: 'Number 2',
  b: 'Bravo',
  1: 'Number 1'
}

console.log('Object.keys:', Object.keys(mixedObj))
console.log('for...in:', (() => {
  const keys = []
  for (let key in mixedObj) keys.push(key)
  return keys
})())
// Object.keys: ['1', '2', 'a', 'b', 'c']
// for...in: ['1', '2', 'a', 'b', 'c']
// 数字键在前且排序,字符串键在后按插入顺序

1.2 ES6+ 对象顺序规则

// ES6 规范定义的键遍历顺序:
// 1. 数字键(包括负数、浮点数)按数值升序
// 2. 字符串键按插入顺序
// 3. Symbol 键按插入顺序

const es6Obj = {
  '-1': 'minus one',
  '0.5': 'half',
  '2': 'two',
  '1': 'one',
  'b': 'bravo',
  'a': 'alpha',
  [Symbol('sym1')]: 'symbol1',
  'c': 'charlie',
  [Symbol('sym2')]: 'symbol2'
}

const keys = []
for (let key in es6Obj) {
  keys.push(key)
}
console.log('遍历顺序:', keys)
// 输出: ['-1', '0.5', '1', '2', 'b', 'a', 'c']
// Symbol 键不会在 for...in 中出现

二、Vue v-for 遍历对象的机制

2.1 Vue 2 的遍历机制

<template>
  <div>
    <!-- Vue 2 使用 Object.keys() 获取键 -->
    <div v-for="(value, key) in myObject" :key="key">
      {{ key }}: {{ value }}
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      myObject: {
        '3': 'three',
        '1': 'one',
        'z': 'zebra',
        'a': 'apple',
        '2': 'two'
      }
    }
  },
  mounted() {
    console.log('Vue 2 使用的键:', Object.keys(this.myObject))
    // 输出: ['1', '2', '3', 'z', 'a']
    // 顺序: 数字键排序 + 字符串键按创建顺序
  }
}
</script>

2.2 Vue 3 的遍历机制

<template>
  <div>
    <!-- Vue 3 同样使用 Object.keys() -->
    <div v-for="(value, key, index) in myObject" :key="key">
      {{ index }}. {{ key }}: {{ value }}
    </div>
  </div>
</template>

<script setup>
import { reactive } from 'vue'

const myObject = reactive({
  '10': 'ten',
  '2': 'two',
  'banana': '🍌',
  '1': 'one',
  'apple': '🍎'
})

console.log('Vue 3 使用的键:', Object.keys(myObject))
// 输出: ['1', '2', '10', 'banana', 'apple']
// 注意: '10' 在 '2' 后面,因为按数字比较排序
</script>

三、保证遍历顺序的 10 种方法

3.1 方法1:使用计算属性排序(推荐)

<template>
  <div>
    <h3>方法1:计算属性排序</h3>
    
    <!-- 按键名排序 -->
    <div v-for="(value, key) in sortedByKey" :key="key">
      {{ key }}: {{ value }}
    </div>
    
    <!-- 按值排序 -->
    <div v-for="item in sortedByValue" :key="item.key">
      {{ item.key }}: {{ item.value }}
    </div>
    
    <!-- 自定义排序规则 -->
    <div v-for="item in customSorted" :key="item.key">
      {{ item.key }}: {{ item.value }}
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      userData: {
        'age': 25,
        'name': '张三',
        'score': 95,
        'email': 'zhangsan@example.com',
        'created_at': '2023-01-15',
        'z-index': 3,
        'address': '北京市'
      }
    }
  },
  computed: {
    // 1. 按键名字母顺序
    sortedByKey() {
      const obj = this.userData
      const sorted = {}
      Object.keys(obj)
        .sort()
        .forEach(key => {
          sorted[key] = obj[key]
        })
      return sorted
    },
    
    // 2. 按键名长度排序
    sortedByKeyLength() {
      const obj = this.userData
      return Object.keys(obj)
        .sort((a, b) => a.length - b.length)
        .reduce((acc, key) => {
          acc[key] = obj[key]
          return acc
        }, {})
    },
    
    // 3. 按值排序(转换为数组)
    sortedByValue() {
      const obj = this.userData
      return Object.entries(obj)
        .sort(([, valueA], [, valueB]) => {
          if (typeof valueA === 'string' && typeof valueB === 'string') {
            return valueA.localeCompare(valueB)
          }
          return valueA - valueB
        })
        .map(([key, value]) => ({ key, value }))
    },
    
    // 4. 自定义优先级排序
    customSorted() {
      const priority = {
        'name': 1,
        'age': 2,
        'email': 3,
        'score': 4,
        'address': 5,
        'created_at': 6,
        'z-index': 7
      }
      
      return Object.entries(this.userData)
        .sort(([keyA], [keyB]) => {
          const priorityA = priority[keyA] || 999
          const priorityB = priority[keyB] || 999
          return priorityA - priorityB
        })
        .map(([key, value]) => ({ key, value }))
    }
  }
}
</script>

3.2 方法2:使用 Map 保持插入顺序

<template>
  <div>
    <h3>方法2:使用 Map 保持插入顺序</h3>
    
    <!-- Map 保持插入顺序 -->
    <div v-for="[key, value] in myMap" :key="key">
      {{ key }}: {{ value }}
    </div>
    
    <!-- 响应式 Map -->
    <div v-for="[key, value] in reactiveMap" :key="key">
      {{ key }}: {{ value }}
    </div>
  </div>
</template>

<script>
import { reactive } from 'vue'

export default {
  data() {
    return {
      // 普通 Map(Vue 2)
      myMap: new Map([
        ['zebra', '🦓'],
        ['apple', '🍎'],
        ['3', 'three'],
        ['1', 'one'],
        ['banana', '🍌']
      ])
    }
  },
  setup() {
    // 响应式 Map(Vue 3)
    const reactiveMap = reactive(new Map([
      ['zebra', '🦓'],
      ['apple', '🍎'],
      ['3', 'three'],
      ['1', 'one'],
      ['banana', '🍌']
    ]))
    
    // Map 操作示例
    const addToMap = () => {
      reactiveMap.set('cherry', '🍒')
    }
    
    const sortMap = () => {
      const sorted = new Map(
        [...reactiveMap.entries()].sort(([keyA], [keyB]) => 
          keyA.localeCompare(keyB)
        )
      )
      // 清空并重新设置
      reactiveMap.clear()
      sorted.forEach((value, key) => reactiveMap.set(key, value))
    }
    
    return {
      reactiveMap,
      addToMap,
      sortMap
    }
  },
  computed: {
    // 将 Map 转换为数组供 v-for 使用
    mapEntries() {
      return Array.from(this.myMap.entries())
    }
  }
}
</script>

3.3 方法3:使用数组存储顺序信息

<template>
  <div>
    <h3>方法3:使用数组存储顺序</h3>
    
    <!-- 方案A:键数组 + 对象 -->
    <div v-for="key in keyOrder" :key="key">
      {{ key }}: {{ dataObject[key] }}
    </div>
    
    <!-- 方案B:对象数组 -->
    <div v-for="item in orderedItems" :key="item.key">
      {{ item.key }}: {{ item.value }}
    </div>
    
    <!-- 方案C:带排序信息的对象 -->
    <div v-for="item in orderedData" :key="item.id">
      {{ item.key }}: {{ item.value }} (顺序: {{ item.order }})
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      // 方案A:分离的键顺序和对象
      keyOrder: ['name', 'age', 'email', 'score', 'address'],
      dataObject: {
        'name': '张三',
        'age': 25,
        'email': 'zhangsan@example.com',
        'score': 95,
        'address': '北京市'
      },
      
      // 方案B:直接使用对象数组
      orderedItems: [
        { key: 'name', value: '张三' },
        { key: 'age', value: 25 },
        { key: 'email', value: 'zhangsan@example.com' },
        { key: 'score', value: 95 },
        { key: 'address', value: '北京市' }
      ],
      
      // 方案C:包含顺序信息的对象数组
      orderedData: [
        { id: 1, key: 'name', value: '张三', order: 1 },
        { id: 2, key: 'age', value: 25, order: 2 },
        { id: 3, key: 'email', value: 'zhangsan@example.com', order: 3 },
        { id: 4, key: 'score', value: 95, order: 4 },
        { id: 5, key: 'address', value: '北京市', order: 5 }
      ]
    }
  },
  methods: {
    // 动态改变顺序
    moveItemUp(key) {
      const index = this.keyOrder.indexOf(key)
      if (index > 0) {
        const temp = this.keyOrder[index]
        this.keyOrder[index] = this.keyOrder[index - 1]
        this.keyOrder[index - 1] = temp
        
        // 强制更新(Vue 2)
        this.$forceUpdate()
      }
    },
    
    // 排序 orderedItems
    sortByKey() {
      this.orderedItems.sort((a, b) => a.key.localeCompare(b.key))
    },
    
    sortByValue() {
      this.orderedItems.sort((a, b) => {
        if (typeof a.value === 'string' && typeof b.value === 'string') {
          return a.value.localeCompare(b.value)
        }
        return a.value - b.value
      })
    }
  }
}
</script>

3.4 方法4:使用 Lodash 排序工具

<template>
  <div>
    <h3>方法4:使用 Lodash 排序</h3>
    
    <div v-for="(value, key) in sortedByLodash" :key="key">
      {{ key }}: {{ value }}
    </div>
    
    <div v-for="item in sortedByCustom" :key="item.key">
      {{ item.key }}: {{ item.value }}
    </div>
  </div>
</template>

<script>
import _ from 'lodash'

export default {
  data() {
    return {
      config: {
        'debug': true,
        'timeout': 5000,
        'retries': 3,
        'host': 'api.example.com',
        'port': 8080,
        'api_version': 'v2',
        'cache_size': 1000
      }
    }
  },
  computed: {
    // 1. 使用 lodash 的 toPairs 和 sortBy
    sortedByLodash() {
      return _.chain(this.config)
        .toPairs()  // 转换为 [key, value] 数组
        .sortBy([0])  // 按第一个元素(key)排序
        .fromPairs()  // 转换回对象
        .value()
    },
    
    // 2. 按 key 长度排序
    sortedByKeyLength() {
      return _.chain(this.config)
        .toPairs()
        .sortBy([pair => pair[0].length])  // 按 key 长度排序
        .fromPairs()
        .value()
    },
    
    // 3. 自定义排序函数
    sortedByCustom() {
      const priority = {
        'host': 1,
        'port': 2,
        'api_version': 3,
        'timeout': 4,
        'retries': 5,
        'cache_size': 6,
        'debug': 7
      }
      
      return _.chain(this.config)
        .toPairs()
        .sortBy([
          ([key]) => priority[key] || 999,  // 按优先级
          ([key]) => key                    // 次要用 key 排序
        ])
        .map(([key, value]) => ({ key, value }))
        .value()
    },
    
    // 4. 按值类型分组排序
    sortedByValueType() {
      return _.chain(this.config)
        .toPairs()
        .groupBy(([, value]) => typeof value)  // 按值类型分组
        .toPairs()  // 转换为 [类型, 条目数组]
        .sortBy([0])  // 按类型排序
        .flatMap(([, entries]) => 
          entries.sort(([keyA], [keyB]) => keyA.localeCompare(keyB))
        )
        .fromPairs()
        .value()
    }
  }
}
</script>

3.5 方法5:使用自定义指令

<template>
  <div>
    <h3>方法5:自定义有序遍历指令</h3>
    
    <!-- 使用自定义指令 -->
    <div v-for="(value, key) in myObject" 
         v-ordered:key 
         :key="key">
      {{ key }}: {{ value }}
    </div>
    
    <!-- 指定排序规则 -->
    <div v-for="(value, key) in myObject" 
         v-ordered:value="'desc'"
         :key="key">
      {{ key }}: {{ value }}
    </div>
  </div>
</template>

<script>
// 自定义有序遍历指令
const orderedDirective = {
  beforeMount(el, binding) {
    const parent = el.parentNode
    const items = Array.from(parent.children)
    
    // 获取排序规则
    const sortBy = binding.arg // 'key' 或 'value'
    const order = binding.value || 'asc' // 'asc' 或 'desc'
    
    // 提取数据
    const data = items.map(item => {
      const text = item.textContent
      const match = text.match(/(.+): (.+)/)
      return match ? { key: match[1].trim(), value: match[2].trim(), element: item } : null
    }).filter(Boolean)
    
    // 排序
    data.sort((a, b) => {
      let comparison = 0
      
      if (sortBy === 'key') {
        comparison = a.key.localeCompare(b.key)
      } else if (sortBy === 'value') {
        const valA = isNaN(a.value) ? a.value : Number(a.value)
        const valB = isNaN(b.value) ? b.value : Number(b.value)
        
        if (typeof valA === 'string' && typeof valB === 'string') {
          comparison = valA.localeCompare(valB)
        } else {
          comparison = valA - valB
        }
      }
      
      return order === 'desc' ? -comparison : comparison
    })
    
    // 重新排序 DOM
    data.forEach(item => {
      parent.appendChild(item.element)
    })
  }
}

export default {
  directives: {
    ordered: orderedDirective
  },
  data() {
    return {
      myObject: {
        'zebra': 'Zoo animal',
        'apple': 'Fruit',
        '3': 'Number',
        '1': 'First',
        'banana': 'Yellow fruit'
      }
    }
  }
}
</script>

3.6 方法6:Vue 3 的响应式排序

<template>
  <div>
    <h3>方法6:Vue 3 响应式排序</h3>
    
    <!-- 响应式排序对象 -->
    <div v-for="(value, key) in sortedObject" :key="key">
      {{ key }}: {{ value }}
    </div>
    
    <button @click="changeSortOrder">切换排序</button>
    <button @click="addNewItem">添加新项</button>
  </div>
</template>

<script setup>
import { reactive, computed, watchEffect } from 'vue'

// 原始数据
const rawData = reactive({
  'zebra': { name: 'Zebra', type: 'animal', priority: 3 },
  'apple': { name: 'Apple', type: 'fruit', priority: 1 },
  'banana': { name: 'Banana', type: 'fruit', priority: 2 },
  'carrot': { name: 'Carrot', type: 'vegetable', priority: 4 }
})

// 排序配置
const sortConfig = reactive({
  key: 'priority', // 'priority' | 'name' | 'type'
  order: 'asc'     // 'asc' | 'desc'
})

// 响应式排序对象
const sortedObject = computed(() => {
  const entries = Object.entries(rawData)
  
  entries.sort(([, a], [, b]) => {
    let comparison = 0
    
    if (sortConfig.key === 'priority') {
      comparison = a.priority - b.priority
    } else if (sortConfig.key === 'name') {
      comparison = a.name.localeCompare(b.name)
    } else if (sortConfig.key === 'type') {
      comparison = a.type.localeCompare(b.type)
    }
    
    return sortConfig.order === 'desc' ? -comparison : comparison
  })
  
  // 转换回对象(但顺序在对象中不保留)
  // 所以返回数组供 v-for 使用
  return entries
})

// 方法
const changeSortOrder = () => {
  const keys = ['priority', 'name', 'type']
  const orders = ['asc', 'desc']
  
  const currentKeyIndex = keys.indexOf(sortConfig.key)
  sortConfig.key = keys[(currentKeyIndex + 1) % keys.length]
  
  // 切换 key 时重置 order
  if (currentKeyIndex === keys.length - 1) {
    const currentOrderIndex = orders.indexOf(sortConfig.order)
    sortConfig.order = orders[(currentOrderIndex + 1) % orders.length]
  }
}

const addNewItem = () => {
  const fruits = ['grape', 'orange', 'kiwi', 'mango']
  const randomFruit = fruits[Math.floor(Math.random() * fruits.length)]
  
  rawData[randomFruit] = {
    name: randomFruit.charAt(0).toUpperCase() + randomFruit.slice(1),
    type: 'fruit',
    priority: Object.keys(rawData).length + 1
  }
}

// 监听排序变化
watchEffect(() => {
  console.log('当前排序:', sortConfig.key, sortConfig.order)
  console.log('排序结果:', sortedObject.value)
})
</script>

3.7 方法7:服务端排序

<template>
  <div>
    <h3>方法7:服务端排序</h3>
    
    <!-- 显示排序后的数据 -->
    <div v-for="item in sortedData" :key="item.key">
      {{ item.key }}: {{ item.value }}
    </div>
    
    <!-- 排序选项 -->
    <div class="sort-controls">
      <button @click="fetchData('key')">按键排序</button>
      <button @click="fetchData('value')">按值排序</button>
      <button @click="fetchData('created_at')">按创建时间</button>
    </div>
  </div>
</template>

<script>
import axios from 'axios'

export default {
  data() {
    return {
      rawData: {},
      sortedData: [],
      isLoading: false,
      currentSort: 'key'
    }
  },
  
  created() {
    this.fetchData()
  },
  
  methods: {
    async fetchData(sortBy = 'key') {
      this.isLoading = true
      this.currentSort = sortBy
      
      try {
        // 调用API获取排序后的数据
        const response = await axios.get('/api/data', {
          params: {
            sort_by: sortBy,
            order: 'asc'
          }
        })
        
        this.rawData = response.data
        
        // 转换为数组供 v-for 使用
        this.sortedData = Object.entries(this.rawData)
          .map(([key, value]) => ({ key, value }))
          
      } catch (error) {
        console.error('获取数据失败:', error)
      } finally {
        this.isLoading = false
      }
    },
    
    // 模拟API响应格式
    mockApiResponse(sortBy) {
      // 模拟服务端排序逻辑
      const data = {
        'user_003': { name: 'Charlie', score: 85, created_at: '2023-03-01' },
        'user_001': { name: 'Alice', score: 95, created_at: '2023-01-01' },
        'user_002': { name: 'Bob', score: 90, created_at: '2023-02-01' }
      }
      
      const entries = Object.entries(data)
      
      // 服务端排序逻辑
      entries.sort(([keyA, valueA], [keyB, valueB]) => {
        if (sortBy === 'key') {
          return keyA.localeCompare(keyB)
        } else if (sortBy === 'value') {
          return valueA.name.localeCompare(valueB.name)
        } else if (sortBy === 'created_at') {
          return new Date(valueA.created_at) - new Date(valueB.created_at)
        }
        return 0
      })
      
      // 转换为对象(按顺序)
      const result = {}
      entries.forEach(([key, value]) => {
        result[key] = value
      })
      
      return result
    }
  }
}
</script>

3.8 方法8:使用 IndexedDB 存储顺序

<template>
  <div>
    <h3>方法8:IndexedDB 存储顺序</h3>
    
    <!-- 显示数据 -->
    <div v-for="item in sortedItems" :key="item.id">
      {{ item.key }}: {{ item.value }}
      <button @click="moveUp(item.id)">上移</button>
      <button @click="moveDown(item.id)">下移</button>
    </div>
    
    <button @click="addItem">添加新项</button>
    <button @click="saveOrder">保存顺序</button>
  </div>
</template>

<script>
// IndexedDB 工具类
class OrderDB {
  constructor(dbName = 'OrderDB', storeName = 'items') {
    this.dbName = dbName
    this.storeName = storeName
    this.db = null
  }
  
  async open() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.dbName, 1)
      
      request.onupgradeneeded = (event) => {
        const db = event.target.result
        if (!db.objectStoreNames.contains(this.storeName)) {
          const store = db.createObjectStore(this.storeName, { keyPath: 'id' })
          store.createIndex('order', 'order', { unique: false })
        }
      }
      
      request.onsuccess = (event) => {
        this.db = event.target.result
        resolve(this.db)
      }
      
      request.onerror = (event) => {
        reject(event.target.error)
      }
    })
  }
  
  async saveOrder(items) {
    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([this.storeName], 'readwrite')
      const store = transaction.objectStore(this.storeName)
      
      // 清空现有数据
      const clearRequest = store.clear()
      
      clearRequest.onsuccess = () => {
        // 保存新数据
        items.forEach((item, index) => {
          item.order = index
          store.put(item)
        })
        
        transaction.oncomplete = () => resolve()
        transaction.onerror = (event) => reject(event.target.error)
      }
      
      clearRequest.onerror = (event) => reject(event.target.error)
    })
  }
  
  async loadOrder() {
    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([this.storeName], 'readonly')
      const store = transaction.objectStore(this.storeName)
      const index = store.index('order')
      const request = index.getAll()
      
      request.onsuccess = (event) => {
        resolve(event.target.result)
      }
      
      request.onerror = (event) => {
        reject(event.target.error)
      }
    })
  }
}

export default {
  data() {
    return {
      db: null,
      items: [
        { id: 1, key: 'name', value: '张三', order: 0 },
        { id: 2, key: 'age', value: 25, order: 1 },
        { id: 3, key: 'email', value: 'zhangsan@example.com', order: 2 },
        { id: 4, key: 'score', value: 95, order: 3 }
      ]
    }
  },
  
  computed: {
    sortedItems() {
      return [...this.items].sort((a, b) => a.order - b.order)
    }
  },
  
  async created() {
    this.db = new OrderDB()
    await this.db.open()
    
    // 尝试加载保存的顺序
    const savedItems = await this.db.loadOrder()
    if (savedItems && savedItems.length > 0) {
      this.items = savedItems
    }
  },
  
  methods: {
    moveUp(id) {
      const index = this.items.findIndex(item => item.id === id)
      if (index > 0) {
        const temp = this.items[index].order
        this.items[index].order = this.items[index - 1].order
        this.items[index - 1].order = temp
      }
    },
    
    moveDown(id) {
      const index = this.items.findIndex(item => item.id === id)
      if (index < this.items.length - 1) {
        const temp = this.items[index].order
        this.items[index].order = this.items[index + 1].order
        this.items[index + 1].order = temp
      }
    },
    
    addItem() {
      const newId = Math.max(...this.items.map(item => item.id)) + 1
      this.items.push({
        id: newId,
        key: `item_${newId}`,
        value: `值 ${newId}`,
        order: this.items.length
      })
    },
    
    async saveOrder() {
      await this.db.saveOrder(this.items)
      alert('顺序已保存到本地数据库')
    }
  }
}
</script>

3.9 方法9:Web Worker 后台排序

// worker.js
self.onmessage = function(event) {
  const { data, sortBy, order } = event.data
  
  // 在 Worker 中进行复杂的排序计算
  const sorted = sortData(data, sortBy, order)
  
  self.postMessage(sorted)
}

function sortData(data, sortBy, order = 'asc') {
  const entries = Object.entries(data)
  
  entries.sort(([keyA, valueA], [keyB, valueB]) => {
    let comparison = 0
    
    // 复杂的排序逻辑
    if (sortBy === 'complex') {
      // 模拟复杂计算
      const weightA = calculateWeight(keyA, valueA)
      const weightB = calculateWeight(keyB, valueB)
      comparison = weightA - weightB
    } else if (sortBy === 'key') {
      comparison = keyA.localeCompare(keyB)
    } else if (sortBy === 'value') {
      comparison = JSON.stringify(valueA).localeCompare(JSON.stringify(valueB))
    }
    
    return order === 'desc' ? -comparison : comparison
  })
  
  // 转换回对象
  const result = {}
  entries.forEach(([key, value]) => {
    result[key] = value
  })
  
  return result
}

function calculateWeight(key, value) {
  // 复杂的权重计算
  let weight = 0
  weight += key.length * 10
  weight += JSON.stringify(value).length
  return weight
}
<template>
  <div>
    <h3>方法9:Web Worker 后台排序</h3>
    
    <div v-for="(value, key) in sortedData" :key="key">
      {{ key }}: {{ value }}
    </div>
    
    <button @click="startComplexSort" :disabled="isSorting">
      {{ isSorting ? '排序中...' : '开始复杂排序' }}
    </button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      originalData: {
        // 大量数据
        'item_001': { value: Math.random(), timestamp: Date.now() },
        'item_002': { value: Math.random(), timestamp: Date.now() },
        // ... 更多数据
      },
      sortedData: {},
      worker: null,
      isSorting: false
    }
  },
  
  created() {
    this.initWorker()
    this.sortedData = { ...this.originalData }
    
    // 生成测试数据
    for (let i = 1; i <= 1000; i++) {
      const key = `item_${i.toString().padStart(3, '0')}`
      this.originalData[key] = {
        value: Math.random(),
        timestamp: Date.now() - Math.random() * 1000000,
        weight: Math.random() * 100
      }
    }
  },
  
  methods: {
    initWorker() {
      if (typeof Worker !== 'undefined') {
        this.worker = new Worker('worker.js')
        
        this.worker.onmessage = (event) => {
          this.sortedData = event.data
          this.isSorting = false
          console.log('Worker 排序完成')
        }
        
        this.worker.onerror = (error) => {
          console.error('Worker 错误:', error)
          this.isSorting = false
        }
      }
    },
    
    startComplexSort() {
      if (!this.worker) {
        console.warn('Worker 不支持,使用主线程排序')
        this.sortInMainThread()
        return
      }
      
      this.isSorting = true
      this.worker.postMessage({
        data: this.originalData,
        sortBy: 'complex',
        order: 'asc'
      })
    },
    
    sortInMainThread() {
      this.isSorting = true
      
      // 模拟复杂计算
      setTimeout(() => {
        const entries = Object.entries(this.originalData)
        entries.sort(([keyA, valueA], [keyB, valueB]) => {
          const weightA = keyA.length * 10 + JSON.stringify(valueA).length
          const weightB = keyB.length * 10 + JSON.stringify(valueB).length
          return weightA - weightB
        })
        
        const result = {}
        entries.forEach(([key, value]) => {
          result[key] = value
        })
        
        this.sortedData = result
        this.isSorting = false
      }, 1000)
    }
  },
  
  beforeDestroy() {
    if (this.worker) {
      this.worker.terminate()
    }
  }
}
</script>

3.10 方法10:综合解决方案

<template>
  <div>
    <h3>方法10:综合解决方案</h3>
    
    <!-- 排序控制器 -->
    <div class="sort-controls">
      <select v-model="sortConfig.by">
        <option value="key">按键名</option>
        <option value="value">按值</option>
        <option value="custom">自定义</option>
      </select>
      
      <select v-model="sortConfig.order">
        <option value="asc">升序</option>
        <option value="desc">降序</option>
      </select>
      
      <button @click="saveSortPreference">保存偏好</button>
    </div>
    
    <!-- 显示数据 -->
    <div class="data-grid">
      <div 
        v-for="item in sortedItems" 
        :key="item.id"
        class="data-item"
        :draggable="true"
        @dragstart="dragStart(item.id)"
        @dragover.prevent
        @drop="drop(item.id)"
      >
        <div class="item-content">
          <span class="item-key">{{ item.key }}</span>
          <span class="item-value">{{ item.value }}</span>
        </div>
        <div class="item-actions">
          <button @click="moveUp(item.id)">↑</button>
          <button @click="moveDown(item.id)">↓</button>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import { throttle } from 'lodash'

export default {
  data() {
    return {
      // 原始数据
      items: [
        { id: 'name', value: '张三', order: 0, type: 'string' },
        { id: 'age', value: 25, order: 1, type: 'number' },
        { id: 'email', value: 'zhangsan@example.com', order: 2, type: 'string' },
        { id: 'score', value: 95, order: 3, type: 'number' },
        { id: 'active', value: true, order: 4, type: 'boolean' }
      ],
      
      // 排序配置
      sortConfig: {
        by: localStorage.getItem('sort_by') || 'key',
        order: localStorage.getItem('sort_order') || 'asc'
      },
      
      // 拖拽状态
      dragItemId: null
    }
  },
  
  computed: {
    // 综合排序
    sortedItems() {
      let items = [...this.items]
      
      // 按配置排序
      switch (this.sortConfig.by) {
        case 'key':
          items.sort((a, b) => {
            const comparison = a.id.localeCompare(b.id)
            return this.sortConfig.order === 'asc' ? comparison : -comparison
          })
          break
          
        case 'value':
          items.sort((a, b) => {
            let comparison = 0
            
            if (a.type === 'string' && b.type === 'string') {
              comparison = a.value.localeCompare(b.value)
            } else {
              comparison = a.value - b.value
            }
            
            return this.sortConfig.order === 'asc' ? comparison : -comparison
          })
          break
          
        case 'custom':
          // 使用保存的顺序
          items.sort((a, b) => a.order - b.order)
          break
      }
      
      return items
    }
  },
  
  watch: {
    // 监听排序配置变化
    sortConfig: {
      handler: throttle(function(newConfig) {
        this.saveSortPreference()
      }, 1000),
      deep: true
    }
  },
  
  methods: {
    // 保存排序偏好
    saveSortPreference() {
      localStorage.setItem('sort_by', this.sortConfig.by)
      localStorage.setItem('sort_order', this.sortConfig.order)
      
      // 保存自定义顺序
      if (this.sortConfig.by === 'custom') {
        localStorage.setItem('custom_order', 
          JSON.stringify(this.items.map(item => item.id))
        )
      }
    },
    
    // 拖拽相关
    dragStart(itemId) {
      this.dragItemId = itemId
    },
    
    drop(targetItemId) {
      if (!this.dragItemId || this.dragItemId === targetItemId) return
      
      const dragIndex = this.items.findIndex(item => item.id === this.dragItemId)
      const targetIndex = this.items.findIndex(item => item.id === targetItemId)
      
      if (dragIndex > -1 && targetIndex > -1) {
        // 交换顺序值
        const tempOrder = this.items[dragIndex].order
        this.items[dragIndex].order = this.items[targetIndex].order
        this.items[targetIndex].order = tempOrder
        
        // 切换到自定义排序
        this.sortConfig.by = 'custom'
        
        // 重置拖拽状态
        this.dragItemId = null
      }
    },
    
    // 移动项目
    moveUp(itemId) {
      const index = this.items.findIndex(item => item.id === itemId)
      if (index > 0) {
        const tempOrder = this.items[index].order
        this.items[index].order = this.items[index - 1].order
        this.items[index - 1].order = tempOrder
        
        this.sortConfig.by = 'custom'
      }
    },
    
    moveDown(itemId) {
      const index = this.items.findIndex(item => item.id === itemId)
      if (index < this.items.length - 1) {
        const tempOrder = this.items[index].order
        this.items[index].order = this.items[index + 1].order
        this.items[index + 1].order = tempOrder
        
        this.sortConfig.by = 'custom'
      }
    },
    
    // 从本地存储加载自定义顺序
    loadCustomOrder() {
      const savedOrder = localStorage.getItem('custom_order')
      if (savedOrder) {
        const orderArray = JSON.parse(savedOrder)
        
        orderArray.forEach((itemId, index) => {
          const item = this.items.find(item => item.id === itemId)
          if (item) {
            item.order = index
          }
        })
        
        // 确保所有项目都有顺序值
        this.items.forEach((item, index) => {
          if (item.order === undefined) {
            item.order = orderArray.length + index
          }
        })
      }
    }
  },
  
  mounted() {
    this.loadCustomOrder()
  }
}
</script>

<style scoped>
.sort-controls {
  margin-bottom: 20px;
  padding: 10px;
  background: #f5f5f5;
  border-radius: 4px;
}

.sort-controls select {
  margin-right: 10px;
  padding: 5px 10px;
}

.data-grid {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.data-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
  background: white;
  cursor: move;
  transition: all 0.3s ease;
}

.data-item:hover {
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.data-item.dragging {
  opacity: 0.5;
}

.item-content {
  display: flex;
  gap: 20px;
}

.item-key {
  font-weight: bold;
  color: #1890ff;
}

.item-value {
  color: #666;
}

.item-actions button {
  margin-left: 5px;
  padding: 2px 8px;
  border: 1px solid #ddd;
  background: white;
  cursor: pointer;
  border-radius: 2px;
}

.item-actions button:hover {
  background: #f0f0f0;
}
</style>

四、总结与最佳实践

4.1 方法选择指南

// 根据需求选择合适的方法
const methodSelectionGuide = {
  // 简单场景
  简单排序: '使用计算属性 + Object.keys().sort()',
  
  // 需要保持插入顺序
  保持顺序: '使用 Map 或数组存储',
  
  // 大量数据
  大数据量: '使用 Web Worker 或服务端排序',
  
  // 用户自定义顺序
  用户排序: '使用拖拽 + 本地存储',
  
  // 复杂业务逻辑
  复杂排序: '使用 Lodash 或自定义算法',
  
  // 实时响应
  实时响应: 'Vue 3 computed + 响应式',
  
  // 持久化需求
  持久化: 'IndexedDB 或后端存储'
}

4.2 性能优化建议

// 1. 缓存排序结果
const cachedSortedData = computed(() => {
  // 添加缓存逻辑
  const cacheKey = JSON.stringify(sortConfig)
  if (cache[cacheKey] && !dataChanged) {
    return cache[cacheKey]
  }
  
  const result = doComplexSort(data, sortConfig)
  cache[cacheKey] = result
  dataChanged = false
  return result
})

// 2. 防抖排序操作
const debouncedSort = _.debounce(() => {
  // 排序逻辑
}, 300)

// 3. 虚拟滚动(大数据量)
import { VirtualScroller } from 'vue-virtual-scroller'

// 4. 分页排序
const paginatedData = computed(() => {
  const sorted = sortedData.value
  const start = (currentPage.value - 1) * pageSize.value
  const end = start + pageSize.value
  return sorted.slice(start, end)
})

4.3 关键结论

  1. Vue v-for 遍历对象顺序:遵循 JavaScript 的 Object.keys() 顺序规则
  2. 默认顺序:数字键排序 + 字符串键插入顺序
  3. 保证顺序的最佳实践
    • 小数据:使用计算属性排序
    • 需要顺序保持:使用 Map 或数组
    • 用户自定义:实现拖拽排序 + 持久化
    • 大数据:使用 Web Worker 或服务端排序

记住核心原则:JavaScript 对象本身是无序的,如果需要确定的遍历顺序,必须显式地管理顺序信息。选择最适合你应用场景的方法,让数据展示既高效又符合用户期望!

Vue 路由跳转完全指南:8种跳转方式深度解析

Vue 路由跳转完全指南:8种跳转方式深度解析

Vue Router 提供了丰富灵活的路由跳转方式,从最简单的链接到最复杂的编程式导航。本文将全面解析所有跳转方式,并给出最佳实践建议。

一、快速概览:8种跳转方式对比

方式 类型 特点 适用场景
1. <router-link> 声明式 最简单,语义化 菜单、导航链接
2. router.push() 编程式 灵活,可带参数 按钮点击、条件跳转
3. router.replace() 编程式 替换历史记录 登录后跳转、表单提交
4. router.go() 编程式 历史记录导航 前进后退、面包屑
5. 命名路由 声明式/编程式 解耦路径 大型项目、重构友好
6. 路由别名 声明式 多个路径指向同一路由 兼容旧URL、SEO优化
7. 重定向 配置式 自动跳转 默认路由、权限控制
8. 导航守卫 拦截式 控制跳转流程 权限验证、数据预取

二、声明式导航:<router-link>

2.1 基础用法

<template>
  <div class="navigation">
    <!-- 1. 基础路径跳转 -->
    <router-link to="/home">首页</router-link>
    
    <!-- 2. 带查询参数 -->
    <router-link to="/user?tab=profile&page=2">
      用户(第2页)
    </router-link>
    
    <!-- 3. 带哈希 -->
    <router-link to="/about#team">关于我们(团队)</router-link>
    
    <!-- 4. 动态路径 -->
    <router-link :to="`/product/${productId}`">
      产品详情
    </router-link>
    
    <!-- 5. 自定义激活样式 -->
    <router-link 
      to="/dashboard" 
      active-class="active-link"
      exact-active-class="exact-active"
    >
      控制面板
    </router-link>
    
    <!-- 6. 替换历史记录 -->
    <router-link to="/login" replace>
      登录(无返回)
    </router-link>
    
    <!-- 7. 自定义标签 -->
    <router-link to="/help" custom v-slot="{ navigate, isActive }">
      <button 
        @click="navigate" 
        :class="{ active: isActive }"
        class="custom-button"
      >
        帮助中心
      </button>
    </router-link>
  </div>
</template>

<script>
export default {
  data() {
    return {
      productId: 123
    }
  }
}
</script>

<style scoped>
.active-link {
  color: #1890ff;
  font-weight: bold;
}

.exact-active {
  border-bottom: 2px solid #1890ff;
}

.custom-button {
  padding: 8px 16px;
  background: #f5f5f5;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.custom-button.active {
  background: #1890ff;
  color: white;
}
</style>

2.2 高级特性

<template>
  <!-- 1. 事件监听 -->
  <router-link 
    to="/cart" 
    @click="handleClick"
    @mouseenter="handleHover"
  >
    购物车
  </router-link>
  
  <!-- 2. 禁止跳转 -->
  <router-link 
    to="/restricted" 
    :event="hasPermission ? 'click' : ''"
    :class="{ disabled: !hasPermission }"
  >
    管理员入口
  </router-link>
  
  <!-- 3. 组合式API使用 -->
  <router-link 
    v-for="nav in navList" 
    :key="nav.path"
    :to="nav.path"
    :class="getNavClass(nav)"
  >
    {{ nav.name }}
    <span v-if="nav.badge" class="badge">{{ nav.badge }}</span>
  </router-link>
</template>

<script setup>
import { computed } from 'vue'
import { useRoute } from 'vue-router'

const route = useRoute()
const hasPermission = computed(() => true) // 权限逻辑

const navList = [
  { path: '/', name: '首页', exact: true },
  { path: '/products', name: '产品', badge: 'New' },
  { path: '/about', name: '关于' }
]

const getNavClass = (nav) => {
  const isActive = nav.exact 
    ? route.path === nav.path
    : route.path.startsWith(nav.path)
  
  return {
    'nav-item': true,
    'nav-active': isActive,
    'has-badge': !!nav.badge
  }
}
</script>

三、编程式导航

3.1 router.push() - 最常用的跳转

// 方法1:路径字符串
router.push('/home')
router.push('/user/123')
router.push('/search?q=vue')
router.push('/about#contact')

// 方法2:路由对象(推荐)
router.push({
  path: '/user/123'
})

// 方法3:命名路由(最佳实践)
router.push({
  name: 'UserProfile',
  params: { id: 123 }
})

// 方法4:带查询参数
router.push({
  path: '/search',
  query: {
    q: 'vue router',
    page: 2,
    sort: 'desc'
  }
})

// 方法5:带哈希
router.push({
  path: '/document',
  hash: '#installation'
})

// 方法6:带状态(不显示在URL中)
router.push({
  name: 'Checkout',
  state: {
    cartItems: ['item1', 'item2'],
    discountCode: 'SAVE10'
  }
})

// 方法7:动态路径
const userId = 456
const userType = 'vip'
router.push({
  path: `/user/${userId}`,
  query: { type: userType }
})

// 方法8:条件跳转
function navigateTo(target) {
  if (userStore.isLoggedIn) {
    router.push(target)
  } else {
    router.push({
      path: '/login',
      query: { redirect: target.path || target }
    })
  }
}

3.2 router.replace() - 替换当前历史记录

// 场景1:登录后跳转(不让用户返回登录页)
function handleLogin() {
  login().then(() => {
    router.replace('/dashboard') // 替换登录页记录
  })
}

// 场景2:表单提交后
function submitForm() {
  submit().then(() => {
    // 提交成功后,替换当前页
    router.replace({
      name: 'Success',
      query: { formId: this.formId }
    })
  })
}

// 场景3:重定向中间页
// 访问 /redirect?target=/dashboard
router.beforeEach((to, from, next) => {
  if (to.path === '/redirect') {
    const target = to.query.target
    router.replace(target || '/')
    return
  }
  next()
})

// 场景4:错误页面处理
function loadProduct(id) {
  fetchProduct(id).catch(error => {
    // 错误时替换到错误页
    router.replace({
      name: 'Error',
      params: { message: '产品加载失败' }
    })
  })
}

3.3 router.go() - 历史记录导航

// 前进后退
router.go(1)   // 前进1步
router.go(-1)  // 后退1步
router.go(-3)  // 后退3步
router.go(0)   // 刷新当前页

// 快捷方法
router.back()     // 后退 = router.go(-1)
router.forward()  // 前进 = router.go(1)

// 实际应用
const navigationHistory = []

// 记录导航历史
router.afterEach((to, from) => {
  navigationHistory.push({
    from: from.fullPath,
    to: to.fullPath,
    timestamp: Date.now()
  })
})

// 返回指定步骤
function goBackSteps(steps) {
  if (router.currentRoute.value.meta.preventBack) {
    alert('当前页面禁止返回')
    return
  }
  
  router.go(-steps)
}

// 返回首页
function goHome() {
  const currentDepth = navigationHistory.length
  router.go(-currentDepth + 1) // 保留首页
}

// 面包屑导航
const breadcrumbs = computed(() => {
  const paths = []
  let current = router.currentRoute.value
  
  while (current) {
    paths.unshift(current)
    // 根据meta中的parent字段查找父路由
    current = routes.find(r => r.name === current.meta?.parent)
  }
  
  return paths
})

3.4 编程式导航最佳实践

// 1. 封装导航工具函数
export const nav = {
  // 带权限检查的跳转
  pushWithAuth(to, requiredRole = null) {
    if (!authStore.isLoggedIn) {
      return router.push({
        path: '/login',
        query: { redirect: typeof to === 'string' ? to : to.path }
      })
    }
    
    if (requiredRole && !authStore.hasRole(requiredRole)) {
      return router.push('/unauthorized')
    }
    
    return router.push(to)
  },
  
  // 带确认的跳转
  pushWithConfirm(to, message = '确定离开当前页面?') {
    return new Promise((resolve) => {
      if (confirm(message)) {
        router.push(to).then(resolve)
      }
    })
  },
  
  // 新标签页打开
  openInNewTab(to) {
    const route = router.resolve(to)
    window.open(route.href, '_blank')
  },
  
  // 带Loading的跳转
  pushWithLoading(to) {
    loadingStore.show()
    return router.push(to).finally(() => {
      loadingStore.hide()
    })
  }
}

// 2. 使用示例
// 组件中使用
methods: {
  viewProductDetail(product) {
    nav.pushWithAuth({
      name: 'ProductDetail',
      params: { id: product.id }
    }, 'user')
  },
  
  editProduct(product) {
    nav.pushWithConfirm(
      { name: 'ProductEdit', params: { id: product.id } },
      '有未保存的更改,确定要编辑吗?'
    )
  }
}

四、命名路由跳转

4.1 配置和使用

// router/index.js
const routes = [
  {
    path: '/',
    name: 'Home',  // 命名路由
    component: Home
  },
  {
    path: '/user/:userId',
    name: 'UserProfile',  // 命名路由
    component: UserProfile,
    props: true
  },
  {
    path: '/product/:category/:id',
    name: 'ProductDetail',  // 命名路由
    component: ProductDetail
  },
  {
    path: '/search',
    name: 'Search',
    component: Search,
    props: route => ({ query: route.query.q })
  }
]

// 组件中使用命名路由
// 声明式
<router-link :to="{ name: 'UserProfile', params: { userId: 123 } }">
  用户资料
</router-link>

// 编程式
router.push({
  name: 'ProductDetail',
  params: {
    category: 'electronics',
    id: 456
  }
})

// 带查询参数
router.push({
  name: 'Search',
  query: {
    q: 'vue router',
    sort: 'price'
  }
})

4.2 命名路由的优势

// 优势1:路径解耦,重构方便
// 旧路径:/user/:id
// 新路径:/profile/:id
// 只需修改路由配置,无需修改跳转代码

// 优势2:清晰的参数传递
router.push({
  name: 'OrderCheckout',
  params: {
    orderId: 'ORD-2024-001',
    step: 'payment'  // 参数名清晰
  },
  query: {
    coupon: 'SAVE20',
    source: 'cart'
  }
})

// 优势3:嵌套路由跳转
const routes = [
  {
    path: '/admin',
    name: 'Admin',
    component: AdminLayout,
    children: [
      {
        path: 'users',
        name: 'AdminUsers',  // 全名:AdminUsers
        component: AdminUsers
      },
      {
        path: 'settings',
        name: 'AdminSettings',
        component: AdminSettings
      }
    ]
  }
]

// 跳转到嵌套路由
router.push({ name: 'AdminUsers' })  // 自动找到完整路径

五、路由别名和重定向

5.1 路由别名

// 多个路径指向同一组件
const routes = [
  {
    path: '/home',
    alias: ['/index', '/main', '/'],  // 多个别名
    component: Home,
    meta: { title: '首页' }
  },
  {
    path: '/about-us',
    alias: '/company',  // 单个别名
    component: About
  },
  {
    path: '/products/:id',
    alias: '/items/:id',  // 带参数的别名
    component: ProductDetail
  }
]

// 实际应用场景
const routes = [
  // 场景1:SEO优化 - 多个关键词
  {
    path: '/vue-tutorial',
    alias: ['/vue-教程', '/vue-入门', '/vue-guide'],
    component: Tutorial
  },
  
  // 场景2:兼容旧URL
  {
    path: '/new-url',
    alias: ['/old-url', '/legacy-url', '/deprecated-path'],
    component: NewComponent,
    meta: { 
      canonical: '/new-url',  // 告诉搜索引擎主URL
      redirect301: true 
    }
  },
  
  // 场景3:多语言路径
  {
    path: '/en/about',
    alias: ['/zh/about', '/ja/about', '/ko/about'],
    component: About,
    beforeEnter(to, from, next) {
      // 根据路径设置语言
      const lang = to.path.split('/')[1]
      i18n.locale = lang
      next()
    }
  }
]

5.2 路由重定向

// 1. 简单重定向
const routes = [
  {
    path: '/home',
    redirect: '/dashboard'  // 访问/home跳转到/dashboard
  },
  {
    path: '/',
    redirect: '/home'  // 根路径重定向
  }
]

// 2. 命名路由重定向
const routes = [
  {
    path: '/user',
    redirect: { name: 'UserList' }  // 重定向到命名路由
  }
]

// 3. 函数式重定向(动态)
const routes = [
  {
    path: '/user/:id',
    redirect: to => {
      // 根据参数动态重定向
      const userType = getUserType(to.params.id)
      if (userType === 'admin') {
        return { name: 'AdminProfile', params: { id: to.params.id } }
      } else {
        return { name: 'UserProfile', params: { id: to.params.id } }
      }
    }
  }
]

// 4. 实际应用场景
const routes = [
  // 场景1:版本升级重定向
  {
    path: '/v1/products/:id',
    redirect: to => `/products/${to.params.id}?version=v1`
  },
  
  // 场景2:权限重定向
  {
    path: '/admin',
    redirect: to => {
      if (authStore.isAdmin) {
        return '/admin/dashboard'
      } else {
        return '/unauthorized'
      }
    }
  },
  
  // 场景3:临时重定向(维护页面)
  {
    path: '/under-maintenance',
    component: Maintenance,
    meta: { maintenance: true }
  },
  {
    path: '/',
    redirect: () => {
      if (isMaintenanceMode) {
        return '/under-maintenance'
      }
      return '/home'
    }
  },
  
  // 场景4:404页面捕获
  {
    path: '/:pathMatch(.*)*',  // 捕获所有未匹配路径
    name: 'NotFound',
    component: NotFound,
    beforeEnter(to, from, next) {
      // 记录404访问
      log404(to.fullPath)
      next()
    }
  }
]

六、导航守卫控制跳转

6.1 完整的守卫流程

// 完整的导航解析流程
const router = createRouter({
  routes,
  // 全局配置
})

// 1. 导航被触发
// 2. 在失活的组件里调用 beforeRouteLeave 守卫
// 3. 调用全局的 beforeEach 守卫
// 4. 在重用的组件里调用 beforeRouteUpdate 守卫
// 5. 在路由配置里调用 beforeEnter 守卫
// 6. 解析异步路由组件
// 7. 在被激活的组件里调用 beforeRouteEnter 守卫
// 8. 调用全局的 beforeResolve 守卫
// 9. 导航被确认
// 10. 调用全局的 afterEach 守卫
// 11. 触发 DOM 更新
// 12. 调用 beforeRouteEnter 守卫中传给 next 的回调函数

// 实际应用:权限控制流程
const routes = [
  {
    path: '/admin',
    component: AdminLayout,
    meta: { requiresAuth: true, requiresAdmin: true },
    beforeEnter: (to, from, next) => {
      // 路由独享守卫
      if (!authStore.isAdmin) {
        next('/unauthorized')
      } else {
        next()
      }
    },
    children: [
      {
        path: 'dashboard',
        component: AdminDashboard,
        meta: { requiresSuperAdmin: true }
      }
    ]
  }
]

// 全局前置守卫
router.beforeEach((to, from, next) => {
  // 1. 页面标题
  document.title = to.meta.title || '默认标题'
  
  // 2. 权限验证
  if (to.meta.requiresAuth && !authStore.isLoggedIn) {
    next({
      path: '/login',
      query: { redirect: to.fullPath }
    })
    return
  }
  
  // 3. 管理员权限
  if (to.meta.requiresAdmin && !authStore.isAdmin) {
    next('/unauthorized')
    return
  }
  
  // 4. 维护模式检查
  if (to.meta.maintenance && !isMaintenanceMode) {
    next(from.path || '/')
    return
  }
  
  // 5. 滚动行为重置
  if (to.meta.resetScroll) {
    window.scrollTo(0, 0)
  }
  
  next()
})

// 全局解析守卫(适合获取数据)
router.beforeResolve(async (to, from, next) => {
  // 预取数据
  if (to.meta.requiresData) {
    try {
      await store.dispatch('fetchRequiredData', to.params)
      next()
    } catch (error) {
      next('/error')
    }
  } else {
    next()
  }
})

// 全局后置守卫
router.afterEach((to, from) => {
  // 1. 页面访问统计
  analytics.trackPageView(to.fullPath)
  
  // 2. 关闭加载动画
  hideLoading()
  
  // 3. 保存导航历史
  saveNavigationHistory(to, from)
  
  // 4. 更新面包屑
  updateBreadcrumb(to)
})

// 组件内守卫
export default {
  beforeRouteEnter(to, from, next) {
    // 不能访问 this,因为组件还没创建
    // 但可以通过回调访问
    next(vm => {
      // 通过 vm 访问组件实例
      vm.loadData(to.params.id)
    })
  },
  
  beforeRouteUpdate(to, from, next) {
    // 在当前路由改变,但是该组件被复用时调用
    // 可以访问组件实例 this
    this.productId = to.params.id
    this.fetchProductData()
    next()
  },
  
  beforeRouteLeave(to, from, next) {
    // 导航离开该组件的对应路由时调用
    // 可以访问组件实例 this
    if (this.hasUnsavedChanges) {
      const answer = confirm('有未保存的更改,确定离开吗?')
      if (!answer) {
        next(false) // 取消导航
        return
      }
    }
    next()
  }
}

6.2 守卫组合实践

// 封装守卫函数
const guard = {
  // 认证守卫
  auth: (to, from, next) => {
    if (!authStore.isLoggedIn) {
      next({
        path: '/login',
        query: { redirect: to.fullPath }
      })
    } else {
      next()
    }
  },
  
  // 权限守卫
  role: (requiredRole) => (to, from, next) => {
    if (!authStore.hasRole(requiredRole)) {
      next('/forbidden')
    } else {
      next()
    }
  },
  
  // 功能开关守卫
  feature: (featureName) => (to, from, next) => {
    if (!featureToggle.isEnabled(featureName)) {
      next('/feature-disabled')
    } else {
      next()
    }
  },
  
  // 数据预取守卫
  prefetch: (dataKey) => async (to, from, next) => {
    try {
      await store.dispatch(`fetch${dataKey}`, to.params)
      next()
    } catch (error) {
      next('/error')
    }
  }
}

// 在路由中使用
const routes = [
  {
    path: '/admin',
    component: AdminLayout,
    beforeEnter: [guard.auth, guard.role('admin')],
    children: [
      {
        path: 'analytics',
        component: Analytics,
        beforeEnter: guard.feature('analytics')
      }
    ]
  },
  {
    path: '/product/:id',
    component: ProductDetail,
    beforeEnter: guard.prefetch('Product')
  }
]

七、高级跳转技巧

7.1 路由传参的多种方式

// 方式1:params(路径参数)
// 路由配置:/user/:id
router.push({ path: '/user/123' })
// 或
router.push({ name: 'User', params: { id: 123 } })

// 方式2:query(查询参数)
router.push({ path: '/search', query: { q: 'vue', page: 2 } })

// 方式3:props(推荐方式)
const routes = [
  {
    path: '/user/:id',
    name: 'User',
    component: User,
    props: true  // params 转为 props
  },
  {
    path: '/product/:id',
    name: 'Product',
    component: Product,
    props: route => ({
      id: Number(route.params.id),
      preview: route.query.preview === 'true'
    })
  }
]

// 方式4:state(不显示在URL中)
router.push({
  name: 'Checkout',
  state: {
    cartItems: [...],
    discount: 'SAVE10',
    source: 'promotion'
  }
})

// 接收state
const route = useRoute()
const cartItems = route.state?.cartItems || []

// 方式5:meta(路由元信息)
const routes = [
  {
    path: '/premium',
    component: Premium,
    meta: {
      requiresSubscription: true,
      subscriptionLevel: 'gold'
    }
  }
]

// 方式6:动态props传递
function navigateWithProps(target, props) {
  // 临时存储props
  const propKey = `temp_props_${Date.now()}`
  sessionStorage.setItem(propKey, JSON.stringify(props))
  
  router.push({
    path: target,
    query: { _props: propKey }
  })
}

// 在目标组件中读取
const route = useRoute()
const propsData = computed(() => {
  const propKey = route.query._props
  if (propKey) {
    const data = JSON.parse(sessionStorage.getItem(propKey) || '{}')
    sessionStorage.removeItem(propKey)
    return data
  }
  return {}
})

7.2 跳转动画和过渡

<template>
  <!-- 路由过渡动画 -->
  <router-view v-slot="{ Component, route }">
    <transition 
      :name="route.meta.transition || 'fade'"
      mode="out-in"
      @before-enter="beforeEnter"
      @after-enter="afterEnter"
    >
      <component :is="Component" :key="route.path" />
    </transition>
  </router-view>
</template>

<script>
export default {
  methods: {
    beforeEnter() {
      // 动画开始前
      document.body.classList.add('page-transition')
    },
    afterEnter() {
      // 动画结束后
      document.body.classList.remove('page-transition')
    }
  }
}
</script>

<style>
/* 淡入淡出 */
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

/* 滑动效果 */
.slide-left-enter-active,
.slide-left-leave-active {
  transition: transform 0.3s ease;
}

.slide-left-enter-from {
  transform: translateX(100%);
}

.slide-left-leave-to {
  transform: translateX(-100%);
}

/* 缩放效果 */
.zoom-enter-active,
.zoom-leave-active {
  transition: all 0.3s ease;
}

.zoom-enter-from {
  opacity: 0;
  transform: scale(0.9);
}

.zoom-leave-to {
  opacity: 0;
  transform: scale(1.1);
}
</style>

7.3 滚动行为控制

const router = createRouter({
  history: createWebHistory(),
  routes,
  
  // 滚动行为控制
  scrollBehavior(to, from, savedPosition) {
    // 1. 返回按钮保持位置
    if (savedPosition) {
      return savedPosition
    }
    
    // 2. 哈希导航
    if (to.hash) {
      return {
        el: to.hash,
        behavior: 'smooth'  // 平滑滚动
      }
    }
    
    // 3. 特定路由滚动到顶部
    if (to.meta.scrollToTop !== false) {
      return { top: 0, behavior: 'smooth' }
    }
    
    // 4. 保持当前位置
    if (to.meta.keepScroll) {
      return false
    }
    
    // 5. 滚动到指定元素
    if (to.meta.scrollTo) {
      return {
        el: to.meta.scrollTo,
        offset: { x: 0, y: 20 }  // 偏移量
      }
    }
    
    // 默认行为
    return { left: 0, top: 0 }
  }
})

八、实际项目应用

8.1 电商网站路由跳转示例

// router/index.js - 电商路由配置
const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home,
    meta: { title: '首页 - 电商平台' }
  },
  {
    path: '/products',
    name: 'ProductList',
    component: ProductList,
    meta: { 
      title: '商品列表',
      keepAlive: true  // 保持组件状态
    },
    props: route => ({
      category: route.query.category,
      sort: route.query.sort || 'default',
      page: parseInt(route.query.page) || 1
    })
  },
  {
    path: '/product/:id(\\d+)',  // 只匹配数字ID
    name: 'ProductDetail',
    component: ProductDetail,
    meta: { 
      title: '商品详情',
      requiresAuth: false
    },
    beforeEnter: async (to, from, next) => {
      // 验证商品是否存在
      try {
        await productStore.fetchProduct(to.params.id)
        next()
      } catch (error) {
        next('/404')
      }
    }
  },
  {
    path: '/cart',
    name: 'ShoppingCart',
    component: ShoppingCart,
    meta: { 
      title: '购物车',
      requiresAuth: true
    }
  },
  {
    path: '/checkout',
    name: 'Checkout',
    component: Checkout,
    meta: { 
      title: '结算',
      requiresAuth: true,
      requiresCart: true  // 需要购物车有商品
    },
    beforeEnter: (to, from, next) => {
      if (cartStore.isEmpty) {
        next({ name: 'ShoppingCart' })
      } else {
        next()
      }
    }
  },
  {
    path: '/order/:orderId',
    name: 'OrderDetail',
    component: OrderDetail,
    meta: { 
      title: '订单详情',
      requiresAuth: true,
      scrollToTop: true
    }
  },
  // ... 其他路由
]

// 组件中使用
export default {
  methods: {
    // 查看商品
    viewProduct(product) {
      this.$router.push({
        name: 'ProductDetail',
        params: { id: product.id },
        query: { 
          source: 'list',
          ref: this.$route.fullPath 
        }
      })
    },
    
    // 加入购物车
    addToCart(product) {
      cartStore.add(product).then(() => {
        // 显示成功提示后跳转
        this.$message.success('加入购物车成功')
        this.$router.push({
          name: 'ShoppingCart',
          query: { added: product.id }
        })
      })
    },
    
    // 立即购买
    buyNow(product) {
      cartStore.add(product).then(() => {
        this.$router.replace({
          name: 'Checkout',
          query: { quick: 'true' }
        })
      })
    },
    
    // 继续购物
    continueShopping() {
      // 返回之前的商品列表,保持筛选状态
      const returnTo = this.$route.query.ref || '/products'
      this.$router.push(returnTo)
    }
  }
}

8.2 后台管理系统路由示例

// 动态路由加载
let dynamicRoutesLoaded = false

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/login',
      name: 'Login',
      component: () => import('@/views/Login.vue'),
      meta: { guest: true }
    },
    {
      path: '/',
      component: Layout,
      children: [
        {
          path: '',
          name: 'Dashboard',
          component: () => import('@/views/Dashboard.vue'),
          meta: { title: '仪表板', icon: 'dashboard' }
        }
      ]
    }
  ]
})

// 动态加载路由
async function loadDynamicRoutes() {
  if (dynamicRoutesLoaded) return
  
  try {
    const userInfo = await authStore.getUserInfo()
    const menus = await menuStore.fetchUserMenus(userInfo.role)
    
    // 转换菜单为路由
    const routes = transformMenusToRoutes(menus)
    
    // 动态添加路由
    routes.forEach(route => {
      router.addRoute('Layout', route)
    })
    
    dynamicRoutesLoaded = true
    
    // 如果当前路由不存在,重定向到首页
    if (!router.hasRoute(router.currentRoute.value.name)) {
      router.replace('/')
    }
  } catch (error) {
    console.error('加载动态路由失败:', error)
    router.push('/error')
  }
}

// 路由守卫
router.beforeEach(async (to, from, next) => {
  // 显示加载中
  loadingBar.start()
  
  // 登录检查
  if (to.meta.requiresAuth && !authStore.isLoggedIn) {
    next({
      name: 'Login',
      query: { redirect: to.fullPath }
    })
    return
  }
  
  // 游客页面检查(已登录用户不能访问登录页)
  if (to.meta.guest && authStore.isLoggedIn) {
    next('/')
    return
  }
  
  // 加载动态路由
  if (!dynamicRoutesLoaded && authStore.isLoggedIn) {
    await loadDynamicRoutes()
    // 动态路由加载后重新跳转
    next(to.fullPath)
    return
  }
  
  // 权限检查
  if (to.meta.permissions) {
    const hasPermission = checkPermission(to.meta.permissions)
    if (!hasPermission) {
      next('/403')
      return
    }
  }
  
  next()
})

router.afterEach((to) => {
  // 设置页面标题
  document.title = to.meta.title ? `${to.meta.title} - 后台管理` : '后台管理'
  
  // 关闭加载
  loadingBar.finish()
  
  // 记录访问日志
  logAccess(to)
})

九、常见问题与解决方案

9.1 路由跳转常见错误

// 错误1:重复跳转相同路由
// ❌ 会报错:NavigationDuplicated
router.push('/current-path')

// ✅ 解决方案:检查当前路由
function safePush(to) {
  if (router.currentRoute.value.path !== to) {
    router.push(to)
  }
}

// 错误2:params 和 path 同时使用
// ❌ params 会被忽略
router.push({
  path: '/user/123',
  params: { id: 456 }  // 这个被忽略!
})

// ✅ 正确:使用命名路由
router.push({
  name: 'User',
  params: { id: 456 }
})

// 错误3:路由未找到
// ❌ 跳转到不存在的路由
router.push('/non-existent')

// ✅ 解决方案:检查路由是否存在
function safeNavigate(to) {
  const resolved = router.resolve(to)
  if (resolved.matched.length > 0) {
    router.push(to)
  } else {
    router.push('/404')
  }
}

// 错误4:组件未加载
// ❌ 异步组件加载失败
router.push({ name: 'AsyncComponent' })

// ✅ 解决方案:添加错误处理
router.push({ name: 'AsyncComponent' }).catch(error => {
  if (error.name === 'NavigationDuplicated') {
    // 忽略重复导航错误
    return
  }
  
  // 其他错误处理
  console.error('导航失败:', error)
  router.push('/error')
})

9.2 性能优化建议

// 1. 路由懒加载
const routes = [
  {
    path: '/heavy-page',
    component: () => import(/* webpackChunkName: "heavy" */ '@/views/HeavyPage.vue')
  }
]

// 2. 组件预加载
// 在适当的时候预加载路由组件
function prefetchRoute(routeName) {
  const route = router.getRoutes().find(r => r.name === routeName)
  if (route && typeof route.components?.default === 'function') {
    route.components.default()
  }
}

// 在鼠标悬停时预加载
<router-link 
  :to="{ name: 'HeavyPage' }"
  @mouseenter="prefetchRoute('HeavyPage')"
>
  重量级页面
</router-link>

// 3. 路由缓存
// 使用 keep-alive 缓存常用页面
<router-view v-slot="{ Component, route }">
  <keep-alive :include="cachedRoutes">
    <component :is="Component" :key="route.fullPath" />
  </keep-alive>
</router-view>

// 4. 滚动位置缓存
const scrollPositions = new Map()

router.beforeEach((to, from) => {
  // 保存离开时的滚动位置
  if (from.meta.keepScroll) {
    scrollPositions.set(from.fullPath, {
      x: window.scrollX,
      y: window.scrollY
    })
  }
})

router.afterEach((to, from) => {
  // 恢复滚动位置
  if (to.meta.keepScroll && from.meta.keepScroll) {
    const position = scrollPositions.get(to.fullPath)
    if (position) {
      window.scrollTo(position.x, position.y)
    }
  }
})

十、总结

路由跳转选择指南

// 根据场景选择跳转方式
const navigationGuide = {
  // 场景:普通链接
  普通链接: '使用 <router-link>',
  
  // 场景:按钮点击跳转
  按钮点击: '使用 router.push()',
  
  // 场景:表单提交后
  表单提交: '使用 router.replace() 避免重复提交',
  
  // 场景:返回上一步
  返回操作: '使用 router.back() 或 router.go(-1)',
  
  // 场景:权限验证后跳转
  权限跳转: '在导航守卫中控制',
  
  // 场景:动态路由
  动态路由: '使用 router.addRoute() 动态添加',
  
  // 场景:404处理
  未找到页面: '配置 catch-all 路由',
  
  // 场景:平滑过渡
  页面过渡: '使用 <transition> 包裹 <router-view>'
}

// 最佳实践总结
const bestPractices = `
1. 尽量使用命名路由,提高代码可维护性
2. 复杂参数传递使用 props 而不是直接操作 $route
3. 重要跳转添加 loading 状态和错误处理
4. 合理使用导航守卫进行权限控制
5. 移动端考虑滑动返回等交互
6. SEO 重要页面使用静态路径
7. 适当使用路由缓存提升性能
8. 监控路由跳转错误和异常
`

Vue Router 提供了强大而灵活的路由跳转机制,掌握各种跳转方式并根据场景合理选择,可以显著提升应用的用户体验和开发效率。记住:简单的用声明式,复杂的用编程式,全局的控制用守卫

Vue Router 中 route 和 router 的终极区别指南

Vue Router 中 route 和 router 的终极区别指南

在 Vue Router 的开发中,routerouter 这两个相似的名字经常让开发者混淆。今天,我们用最直观的方式彻底搞懂它们的区别!

一、最简区分:一句话理解

// 一句话总结:
// route = 当前的路由信息(只读)—— "我在哪?"
// router = 路由的实例对象(可操作)—— "我怎么去?"

// 类比理解:
// route 像 GPS 定位信息:显示当前位置(经纬度、地址等)
// router 像导航系统:提供路线规划、导航、返回等功能

二、核心区别对比表

维度 route router
本质 当前路由信息对象(只读) 路由实例(操作方法集合)
类型 RouteLocationNormalized Router 实例
功能 获取当前路由信息 进行路由操作(跳转、守卫等)
数据流向 信息输入(读取) 指令输出(执行)
修改性 只读,不可直接修改 可操作,可修改路由状态
使用场景 获取参数、查询、路径等信息 跳转、编程式导航、全局配置

三、代码直观对比

3.1 获取方式对比

// 选项式 API
export default {
  // route:通过 this.$route 访问
  mounted() {
    console.log(this.$route)     // 当前路由信息
    console.log(this.$router)    // 路由实例
  }
}

// 组合式 API
import { useRoute, useRouter } from 'vue-router'

export default {
  setup() {
    const route = useRoute()    // 相当于 this.$route
    const router = useRouter()  // 相当于 this.$router
    
    return { route, router }
  }
}

3.2 数据结构对比

// route 对象的结构(简化版)
const route = {
  // 路径信息
  path: '/user/123/profile?tab=settings',
  fullPath: '/user/123/profile?tab=settings#section-2',
  
  // 路由参数(params)
  params: {
    id: '123'  // 来自 /user/:id
  },
  
  // 查询参数(query)
  query: {
    tab: 'settings'  // 来自 ?tab=settings
  },
  
  // 哈希值
  hash: '#section-2',
  
  // 路由元信息
  meta: {
    requiresAuth: true,
    title: '用户设置'
  },
  
  // 匹配的路由记录
  matched: [
    { path: '/', component: Home, meta: { ... } },
    { path: '/user/:id', component: UserLayout, meta: { ... } },
    { path: '/user/:id/profile', component: Profile, meta: { ... } }
  ],
  
  // 路由名称
  name: 'UserProfile',
  
  // 重定向的来源(如果有)
  redirectedFrom: undefined
}

// router 对象的结构(主要方法)
const router = {
  // 核心方法
  push(),        // 导航到新路由
  replace(),     // 替换当前路由
  go(),          // 前进/后退
  back(),        // 后退
  forward(),     // 前进
  
  // 路由信息
  currentRoute,  // 当前路由(相当于route)
  options,       // 路由配置
  
  // 守卫相关
  beforeEach(),
  beforeResolve(),
  afterEach(),
  
  // 其他
  addRoute(),    // 动态添加路由
  removeRoute(), // 移除路由
  hasRoute(),    // 检查路由是否存在
  getRoutes(),   // 获取所有路由
  isReady()      // 检查路由是否就绪
}

四、route:深入了解当前路由信息

4.1 主要属性详解

// 获取完整示例
const route = useRoute()

// 1. 路径相关
console.log('path:', route.path)        // "/user/123"
console.log('fullPath:', route.fullPath) // "/user/123?name=john#about"

// 2. 参数相关(最常用!)
// params:路径参数(必选参数)
console.log('params:', route.params)    // { id: '123', slug: 'vue-guide' }
console.log('id:', route.params.id)     // "123"

// query:查询参数(可选参数)
console.log('query:', route.query)      // { page: '2', sort: 'desc' }
console.log('page:', route.query.page)  // "2"

// hash:哈希值
console.log('hash:', route.hash)        // "#section-1"

// 3. 元信息(meta)
// 路由配置中的 meta 字段
const routes = [
  {
    path: '/admin',
    component: Admin,
    meta: {
      requiresAuth: true,
      permissions: ['admin'],
      breadcrumb: '管理后台'
    }
  }
]

// 使用
if (route.meta.requiresAuth) {
  // 需要认证
}

// 4. 匹配的路由记录
route.matched.forEach(record => {
  console.log('匹配的路由:', record.path)
  // 可以访问嵌套路由的 meta
  if (record.meta.requiresAuth) {
    // 所有匹配的路由都需要认证
  }
})

// 5. 名称和来源
console.log('name:', route.name)            // "UserProfile"
console.log('redirectedFrom:', route.redirectedFrom) // 重定向来源

4.2 实际使用场景

<template>
  <!-- 场景1:根据参数显示内容 -->
  <div v-if="route.params.id">
    用户ID: {{ route.params.id }}
  </div>
  
  <!-- 场景2:根据query显示不同标签 -->
  <div v-if="route.query.tab === 'profile'">
    显示个人资料
  </div>
  <div v-else-if="route.query.tab === 'settings'">
    显示设置
  </div>
  
  <!-- 场景3:动态标题 -->
  <title>{{ pageTitle }}</title>
</template>

<script>
import { useRoute, computed } from 'vue'

export default {
  setup() {
    const route = useRoute()
    
    // 动态标题
    const pageTitle = computed(() => {
      const baseTitle = '我的应用'
      if (route.meta.title) {
        return `${route.meta.title} - ${baseTitle}`
      }
      return baseTitle
    })
    
    // 权限检查
    const hasPermission = computed(() => {
      const userRoles = ['user', 'editor']
      const requiredRoles = route.meta.roles || []
      return requiredRoles.some(role => userRoles.includes(role))
    })
    
    // 面包屑导航
    const breadcrumbs = computed(() => {
      return route.matched
        .filter(record => record.meta.breadcrumb)
        .map(record => ({
          title: record.meta.breadcrumb,
          path: record.path
        }))
    })
    
    return { route, pageTitle, hasPermission, breadcrumbs }
  }
}
</script>

五、router:路由操作和控制

5.1 核心方法详解

const router = useRouter()

// 1. 编程式导航
// push - 添加新的历史记录
router.push('/home')                      // 路径字符串
router.push({ path: '/home' })            // 路径对象
router.push({ name: 'Home' })             // 命名路由
router.push({ 
  name: 'User', 
  params: { id: 123 }, 
  query: { tab: 'profile' },
  hash: '#section-2'
})

// replace - 替换当前历史记录(无返回)
router.replace('/login')
router.replace({ path: '/login', query: { redirect: route.fullPath } })

// go - 在历史记录中前进/后退
router.go(1)    // 前进1步
router.go(-1)   // 后退1步
router.go(-3)   // 后退3步
router.go(0)    // 刷新当前页面

// back/forward - 便捷方法
router.back()     // 后退 = router.go(-1)
router.forward()  // 前进 = router.go(1)

// 2. 动态路由管理
// 添加路由(常用于权限路由)
router.addRoute({
  path: '/admin',
  component: Admin,
  meta: { requiresAuth: true }
})

// 添加嵌套路由
router.addRoute('Admin', {
  path: 'users',
  component: AdminUsers
})

// 移除路由
router.removeRoute('admin') // 通过名称移除

// 检查路由是否存在
if (router.hasRoute('admin')) {
  console.log('管理员路由已存在')
}

// 获取所有路由
const allRoutes = router.getRoutes()
console.log('总路由数:', allRoutes.length)

// 3. 路由守卫
// 全局前置守卫
router.beforeEach((to, from, next) => {
  if (to.meta.requiresAuth && !isAuthenticated()) {
    next('/login')
  } else {
    next()
  }
})

// 全局解析守卫
router.beforeResolve((to, from) => {
  // 所有组件解析完成后调用
})

// 全局后置守卫
router.afterEach((to, from) => {
  // 路由跳转完成后调用
  logPageView(to.fullPath)
})

5.2 实际使用场景

<template>
  <div>
    <!-- 导航按钮 -->
    <button @click="goToHome">返回首页</button>
    <button @click="goToUser(123)">查看用户123</button>
    <button @click="openInNewTab">新标签打开</button>
    <button @click="goBack">返回上一步</button>
    
    <!-- 条件导航 -->
    <button v-if="canEdit" @click="editItem">编辑</button>
    
    <!-- 路由状态 -->
    <p>当前路由: {{ currentRoute.path }}</p>
    <button @click="checkRoutes">检查路由配置</button>
  </div>
</template>

<script>
import { useRouter, useRoute } from 'vue-router'

export default {
  setup() {
    const router = useRouter()
    const route = useRoute()
    
    // 1. 基本导航
    const goToHome = () => {
      router.push('/')
    }
    
    const goToUser = (userId) => {
      router.push({
        name: 'UserProfile',
        params: { id: userId },
        query: { tab: 'details' }
      })
    }
    
    const goBack = () => {
      if (window.history.length > 1) {
        router.back()
      } else {
        router.push('/')
      }
    }
    
    // 2. 条件导航
    const canEdit = computed(() => {
      return route.params.id && userStore.canEdit(route.params.id)
    })
    
    const editItem = () => {
      router.push(`/edit/${route.params.id}`)
    }
    
    // 3. 新标签页打开
    const openInNewTab = () => {
      const routeData = router.resolve({
        name: 'UserProfile',
        params: { id: 123 }
      })
      window.open(routeData.href, '_blank')
    }
    
    // 4. 动态路由管理
    const addAdminRoute = () => {
      if (!router.hasRoute('admin')) {
        router.addRoute({
          path: '/admin',
          name: 'admin',
          component: () => import('./Admin.vue'),
          meta: { requiresAdmin: true }
        })
        console.log('管理员路由已添加')
      }
    }
    
    // 5. 路由状态检查
    const checkRoutes = () => {
      console.log('当前路由:', router.currentRoute.value)
      console.log('所有路由:', router.getRoutes())
      console.log('路由配置:', router.options)
    }
    
    // 6. 路由跳转拦截
    const navigateWithConfirm = async (to) => {
      if (route.meta.hasUnsavedChanges) {
        const confirmed = await confirm('有未保存的更改,确定离开?')
        if (!confirmed) return
      }
      router.push(to)
    }
    
    // 7. 获取路由组件
    const getRouteComponent = () => {
      const matched = route.matched
      const component = matched[matched.length - 1]?.components?.default
      return component
    }
    
    return {
      currentRoute: router.currentRoute,
      goToHome,
      goToUser,
      goBack,
      canEdit,
      editItem,
      openInNewTab,
      addAdminRoute,
      checkRoutes,
      navigateWithConfirm
    }
  }
}
</script>

六、常见误区与正确用法

6.1 错误 vs 正确

// ❌ 错误:试图修改 route
this.$route.params.id = 456  // 不会生效!
this.$route.query.page = '3' // 不会生效!

// ✅ 正确:使用 router 进行导航
this.$router.push({
  params: { id: 456 },
  query: { page: '3' }
})

// ❌ 错误:混淆使用
// 试图用 route 进行跳转
this.$route.push('/home')  // 报错!route 没有 push 方法

// ✅ 正确:分清职责
const id = this.$route.params.id    // 获取信息用 route
this.$router.push(`/user/${id}`)    // 跳转用 router

// ❌ 错误:直接修改 URL
window.location.href = '/new-page'  // 会刷新页面!

// ✅ 正确:使用 router
this.$router.push('/new-page')      // 单页应用跳转

6.2 响应式处理

<template>
  <!-- ❌ 错误:直接监听路由对象 -->
  <!-- 这种方式可能会导致无限循环 -->
  
  <!-- ✅ 正确:使用计算属性或监听器 -->
  <div>
    当前用户: {{ userId }}
    当前页面: {{ currentPage }}
  </div>
</template>

<script>
export default {
  computed: {
    // ✅ 正确:使用计算属性响应式获取
    userId() {
      return this.$route.params.id || 'unknown'
    },
    currentPage() {
      return parseInt(this.$route.query.page) || 1
    }
  },
  
  watch: {
    // ✅ 正确:监听特定参数变化
    '$route.params.id': {
      handler(newId) {
        if (newId) {
          this.loadUser(newId)
        }
      },
      immediate: true
    },
    
    // ✅ 监听整个路由变化(谨慎使用)
    $route(to, from) {
      // 处理路由变化逻辑
      this.trackPageView(to.path)
    }
  },
  
  // ✅ 使用路由守卫
  beforeRouteUpdate(to, from, next) {
    // 在同一组件内响应路由参数变化
    this.loadData(to.params.id)
    next()
  }
}
</script>

七、高级应用场景

7.1 路由元信息和权限控制

// 路由配置
const routes = [
  {
    path: '/',
    component: Home,
    meta: { 
      title: '首页',
      requiresAuth: false 
    }
  },
  {
    path: '/dashboard',
    component: Dashboard,
    meta: { 
      title: '控制面板',
      requiresAuth: true,
      permissions: ['user']
    }
  },
  {
    path: '/admin',
    component: Admin,
    meta: { 
      title: '管理员',
      requiresAuth: true,
      permissions: ['admin'],
      breadcrumb: '管理后台'
    },
    children: [
      {
        path: 'users',
        component: AdminUsers,
        meta: { 
          title: '用户管理',
          breadcrumb: '用户管理'
        }
      }
    ]
  }
]

// 权限控制守卫
router.beforeEach((to, from, next) => {
  const isAuthenticated = checkAuth()
  const userPermissions = getUserPermissions()
  
  // 检查是否需要认证
  if (to.meta.requiresAuth && !isAuthenticated) {
    next({
      path: '/login',
      query: { redirect: to.fullPath }
    })
    return
  }
  
  // 检查权限
  if (to.meta.permissions) {
    const hasPermission = to.meta.permissions.some(perm => 
      userPermissions.includes(perm)
    )
    
    if (!hasPermission) {
      next('/403') // 无权限页面
      return
    }
  }
  
  next()
})

// 组件内使用
export default {
  setup() {
    const route = useRoute()
    const router = useRouter()
    
    // 检查当前路由权限
    const canAccess = computed(() => {
      if (!route.meta.permissions) return true
      return route.meta.permissions.some(perm => 
        userStore.permissions.includes(perm)
      )
    })
    
    // 如果没有权限,重定向
    watchEffect(() => {
      if (!canAccess.value) {
        router.replace('/unauthorized')
      }
    })
    
    return { canAccess }
  }
}

7.2 路由数据预取

// 使用 router 和 route 配合数据预取
const router = createRouter({
  routes,
  scrollBehavior(to, from, savedPosition) {
    // 滚动行为控制
    if (savedPosition) {
      return savedPosition
    }
    return { top: 0 }
  }
})

// 组件数据预取
export default {
  async beforeRouteEnter(to, from, next) {
    // 在进入路由前获取数据
    try {
      const userData = await fetchUser(to.params.id)
      next(vm => {
        vm.user = userData
      })
    } catch (error) {
      next('/error')
    }
  },
  
  async beforeRouteUpdate(to, from, next) {
    // 路由参数变化时更新数据
    this.user = await fetchUser(to.params.id)
    next()
  }
}

7.3 路由状态持久化

// 保存路由状态到 localStorage
const router = createRouter({
  history: createWebHistory(),
  routes
})

// 路由变化时保存状态
router.afterEach((to) => {
  localStorage.setItem('lastRoute', JSON.stringify({
    path: to.path,
    query: to.query,
    params: to.params,
    timestamp: Date.now()
  }))
})

// 应用启动时恢复状态
router.isReady().then(() => {
  const saved = localStorage.getItem('lastRoute')
  if (saved) {
    const lastRoute = JSON.parse(saved)
    // 根据保存的状态做一些处理
    console.log('上次访问:', lastRoute.path)
  }
})

// 组件内使用 route 获取状态
export default {
  setup() {
    const route = useRoute()
    const router = useRouter()
    
    // 保存表单状态到路由 query
    const saveFormState = (formData) => {
      router.push({
        query: {
          ...route.query,
          form: JSON.stringify(formData)
        }
      })
    }
    
    // 从路由 query 恢复表单状态
    const loadFormState = () => {
      if (route.query.form) {
        return JSON.parse(route.query.form)
      }
      return null
    }
    
    return { saveFormState, loadFormState }
  }
}

八、TypeScript 类型支持

// 为 route 和 router 添加类型支持
import { RouteLocationNormalized, Router } from 'vue-router'

// 扩展 Route Meta 类型
declare module 'vue-router' {
  interface RouteMeta {
    // 自定义元字段
    requiresAuth?: boolean
    permissions?: string[]
    breadcrumb?: string
    title?: string
    keepAlive?: boolean
  }
}

// 组件内使用类型
import { useRoute, useRouter } from 'vue-router'

export default defineComponent({
  setup() {
    const route = useRoute() as RouteLocationNormalized
    const router = useRouter() as Router
    
    // 类型安全的参数访问
    const userId = computed(() => {
      // params 类型为 Record<string, string | string[]>
      const id = route.params.id
      if (Array.isArray(id)) {
        return id[0] // 处理数组情况
      }
      return id || ''
    })
    
    // 类型安全的查询参数
    const page = computed(() => {
      const pageStr = route.query.page
      if (Array.isArray(pageStr)) {
        return parseInt(pageStr[0]) || 1
      }
      return parseInt(pageStr || '1')
    })
    
    // 类型安全的导航
    const navigateToUser = (id: string) => {
      router.push({
        name: 'UserProfile',
        params: { id },  // 类型检查
        query: { tab: 'info' as const }  // 字面量类型
      })
    }
    
    return { userId, page, navigateToUser }
  }
})

九、记忆口诀与最佳实践

9.1 记忆口诀

/*
口诀一:
route 是 "看" - 看我在哪,看有什么参数
router 是 "动" - 动去哪,动怎么去

口诀二:
route 三要素:params、query、meta
router 三动作:push、replace、go

口诀三:
读信息找 route,改路由找 router
查状态用 route,变状态用 router
*/

9.2 最佳实践清单

const bestPractices = {
  route: [
    '✅ 使用计算属性包装 route 属性',
    '✅ 使用 watch 监听特定参数变化',
    '✅ 使用 route.meta 进行权限判断',
    '✅ 使用 route.matched 获取嵌套路由信息',
    '❌ 不要直接修改 route 对象',
    '❌ 避免深度监听整个 route 对象'
  ],
  
  router: [
    '✅ 使用命名路由代替路径字符串',
    '✅ 编程式导航时传递完整的路由对象',
    '✅ 使用 router.isReady() 等待路由就绪',
    '✅ 动态路由添加后检查是否存在',
    '❌ 不要混用 window.location 和 router',
    '❌ 避免在循环中频繁调用 router 方法'
  ],
  
  combined: [
    '✅ route 获取信息,router 执行操作',
    '✅ 使用 router.currentRoute 获取当前路由',
    '✅ 在路由守卫中结合两者进行复杂逻辑',
    '✅ 使用 TypeScript 增强类型安全'
  ]
}

总结

route 和 router 的核心区别总结:

方面 route router
角色 信息提供者 行动执行者
数据 当前路由状态快照 路由操作方法集合
修改 只读不可变 可变可操作
类比 GPS 定位信息 导航系统指令
心态 "我现在在哪?" "我要去哪里?怎么去?"

黄金法则:

  • 读信息 → 用 route
  • 做跳转 → 用 router
  • 改状态 → 通过 router 改变,从 route 读取结果

记住:route 告诉你 现在router 带你去 未来。分清它们,你的 Vue Router 使用将更加得心应手!

ReactNative新架构之Android端TurboModule机制完全解析

ReactNative新架构之Android端TurboModule机制完全解析

前言

注意,本文是基于React Native 0.83版本源码进行分析。

《React Native新架构之Android端初始化源码分析》一文已经剖析了启动流程,但上次略过了TurboModule系统,现在就详细分析一下TurboModule系统。

TurboModule 初始化

我们先回顾一下源码react-native/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactInstance.kt

internal class ReactInstance(
    private val context: BridgelessReactContext,
    delegate: ReactHostDelegate,
    componentFactory: ComponentFactory,
    devSupportManager: DevSupportManager,
    exceptionHandler: QueueThreadExceptionHandler,
    useDevSupport: Boolean,
    reactHostInspectorTarget: ReactHostInspectorTarget?,
) {
  @Suppress("NoHungarianNotation") @DoNotStrip private val mHybridData: HybridData

  private val turboModuleManager: TurboModuleManager
  private val javaTimerManager: JavaTimerManager
  private val viewManagerResolver: BridgelessViewManagerResolver

  val reactQueueConfiguration: ReactQueueConfiguration
  val fabricUIManager: FabricUIManager
  val javaScriptContextHolder: JavaScriptContextHolder

  init {
    // 省略……

    // 设置 TurboModules
    Systrace.beginSection(Systrace.TRACE_TAG_REACT, "ReactInstance.initialize#initTurboModules")

    val reactPackages: MutableList<ReactPackage> = ArrayList<ReactPackage>()
    reactPackages.add(
        CoreReactPackage(context.devSupportManager, context.defaultHardwareBackBtnHandler)
    )
    if (useDevSupport) {
      reactPackages.add(DebugCorePackage())
    }
    reactPackages.addAll(delegate.reactPackages)
    // 创建 TurboModuleManagerDelegate
    val turboModuleManagerDelegate =
        delegate.turboModuleManagerDelegateBuilder
            .setPackages(reactPackages)
            .setReactApplicationContext(context)
            .build()

    val unbufferedRuntimeExecutor = getUnbufferedRuntimeExecutor()
    // 创建 TurboModuleManager
    turboModuleManager =
        TurboModuleManager( // 使用 unbuffered RuntimeExecutor 来安装绑定
            unbufferedRuntimeExecutor,
            turboModuleManagerDelegate,
            getJSCallInvokerHolder(),
            getNativeMethodCallInvokerHolder(),
        )
    Systrace.endSection(Systrace.TRACE_TAG_REACT)
    // 省略……
  }
  // 省略……
}

以上代码,在ReactInstance对象构造时立即在 init {} 中创建 TurboModuleManager。但需要注意一点,这里注入的CoreReactPackage平台相关的一些内部模块,而reactPackages.addAll(delegate.reactPackages)添加的则是在MainApplication中注册的本地Turbo Module实现:

class MainApplication : Application(), ReactApplication {

  override val reactHost: ReactHost by
      lazy(LazyThreadSafetyMode.NONE) {
        getDefaultReactHost(
            context = applicationContext,
            packageList =
                PackageList(this).packages.apply {
                  // 例如,目前还无法自动链接的软件包可以手动添加到这里:
                  // add(MyReactNativePackage())
                },
        )
      }
  // 省略……
}

这里需要重点留意的是PackageList(this).packages这行代码,PackageList是工具自动生成的代码,主要依靠Gradle 插件机制来收集和链接三方TurboModule模块,其中主要是Gradle脚本代码,为了不中断代码分析的思路,所以这部分的详细分析我放到本文的最后。

TurboModuleManager的初始化

源码react-native/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/turbomodule/core/TurboModuleManager.kt

/**
 * 这是 TurboModules 的主类和入口点。注意,这是一个混合类,
 * 它有一个 C++ 对应类。此类安装 JSI 绑定。它还实现了获取 Java 模块的方法,该方法由 C++ 对应类调用。
 */
@OptIn(FrameworkAPI::class)
public class TurboModuleManager(
    runtimeExecutor: RuntimeExecutor,
    private val delegate: TurboModuleManagerDelegate?,
    jsCallInvokerHolder: CallInvokerHolder,
    nativeMethodCallInvokerHolder: NativeMethodCallInvokerHolder,
) : TurboModuleRegistry {

    public override val eagerInitModuleNames: List<String>
    private val turboModuleProvider: ModuleProvider
    private val legacyModuleProvider: ModuleProvider

    // 模块清理锁(防止在清理时创建新模块)
    private val moduleCleanupLock = Object()

    @GuardedBy("moduleCleanupLock") 
    private var moduleCleanupStarted = false

    // 模块缓存:moduleName -> ModuleHolder
    @GuardedBy("moduleCleanupLock") 
    private val moduleHolders = mutableMapOf<String, ModuleHolder>()

    // 1. 创建 C++ HybridData(JNI 桥接)
    @DoNotStrip
    private val mHybridData: HybridData = initHybrid(
        runtimeExecutor,
        jsCallInvokerHolder as CallInvokerHolderImpl,
        nativeMethodCallInvokerHolder as NativeMethodCallInvokerHolderImpl,
        delegate,
    )

    init {
        // 2. 安装 JSI Bindings 到 JavaScript Runtime
        installJSIBindings(shouldEnableLegacyModuleInterop())

        // 3. 获取需要预加载的模块列表
        eagerInitModuleNames = delegate?.getEagerInitModuleNames() ?: emptyList()

        // 4. 创建 TurboModule 提供者
        val nullProvider = ModuleProvider { _: String -> null }

        turboModuleProvider = if (delegate == null) nullProvider
        else ModuleProvider { moduleName: String -> 
            delegate.getModule(moduleName) as NativeModule?
        }

        // 5. 创建 Legacy Module 提供者(兼容旧架构)
        // 省略......
    }

    companion object {
        private const val TAG = "TurboModuleManager"

        init {
            // 加载 C++ 库
            SoLoader.loadLibrary("turbomodulejsijni")
        }
    }
}

可以看到,TurboModuleManager实现了TurboModuleRegistry接口,我们可以先看一下该接口了解大致的功能:

/**
 * 用于创建和检索 NativeModule 的接口。
 * 
 * 为什么这个接口要以 "Turbo" 作为前缀,即使它同时支持 Legacy NativeModule 和 TurboModule?
 * 因为已经存在一个 NativeModuleRegistry(旧架构的一部分)。
 * 一旦删除了那个类,我们应该相应地重命名此接口。
 */
public interface TurboModuleRegistry {
    /**
     * 返回名为 `moduleName` 的 NativeModule 实例。
     * 如果 `moduleName` 对应的 TurboModule 尚未实例化,则实例化它。
     * 如果没有注册名为 `moduleName` 的 TurboModule,则返回 null。
     */
    public fun getModule(moduleName: String): NativeModule?

    /** 获取所有已实例化的 NativeModule。*/
    public val modules: Collection<NativeModule>

    /** 检查名为 `moduleName` 的 NativeModule 是否已被实例化。*/
    public fun hasModule(moduleName: String): Boolean

    /**
     * 返回所有应该被预先初始化的 NativeModule 的名称列表。
     * 通过对每个名称调用 getModule,应用程序可以预先初始化这些 NativeModule。
     */
    public val eagerInitModuleNames: List<String>

    /**
     * 在 ReactHost 关闭过程中调用。
     * 此方法在 React Native 停止之前被调用。
     */
    public fun invalidate()
}

接下来仔细分析TurboModuleManager的初始化。initHybridinstallJSIBindings都是Native方法,我们稍后分析,先看看delegate?.getEagerInitModuleNames()返回的预加载模块是什么。这里的delegate是外部传入的TurboModuleManagerDelegate实例,在ReactInstance初始化中创建:

val turboModuleManagerDelegate = delegate.turboModuleManagerDelegateBuilder
                                  .setPackages(reactPackages)
                                  .setReactApplicationContext(context)
                                  .build()

我们知道ReactInstance中的delegateDefaultReactHostDelegate实例,而DefaultReactHostDelegate中的turboModuleManagerDelegateBuilder亦是外部传入:

// DefaultReactHost.kt

val defaultTmmDelegateBuilder = DefaultTurboModuleManagerDelegate.Builder()
cxxReactPackageProviders.forEach { defaultTmmDelegateBuilder.addCxxReactPackage(it) }
val defaultReactHostDelegate =
    DefaultReactHostDelegate(
        jsMainModulePath = jsMainModulePath,
        jsBundleLoader = bundleLoader,
        reactPackages = packageList,
        jsRuntimeFactory = jsRuntimeFactory ?: HermesInstance(),
        bindingsInstaller = bindingsInstaller,
        turboModuleManagerDelegateBuilder = defaultTmmDelegateBuilder,
        exceptionHandler = exceptionHandler,
    )

再看react-native/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/defaults/DefaultTurboModuleManagerDelegate.kt

    override fun build(
        context: ReactApplicationContext,
        packages: List<ReactPackage>,
    ): DefaultTurboModuleManagerDelegate =
        DefaultTurboModuleManagerDelegate(
            context,
            packages,
            cxxReactPackageProviders.flatMap { provider -> provider(context) },
        )

那么delegate?.getEagerInitModuleNames()调用中的delegate实际上就是DefaultTurboModuleManagerDelegate。但这里要注意一下,delegate.turboModuleManagerDelegateBuilder.setPackages(reactPackages).setReactApplicationContext(context).build()实际上调用的是父类中的不带参数的build方法react-native/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactPackageTurboModuleManagerDelegate.kt

    public fun build(): ReactPackageTurboModuleManagerDelegate {
      val nonNullContext =
          requireNotNull(context) {
            "The ReactApplicationContext must be provided to create ReactPackageTurboModuleManagerDelegate"
          }
      val nonNullPackages =
          requireNotNull(packages) {
            "A set of ReactPackages must be provided to create ReactPackageTurboModuleManagerDelegate"
          }
      return build(nonNullContext, nonNullPackages)
    }

由父类的build再调用其子类实现的带两个参数的build方法。所以DefaultTurboModuleManagerDelegate构造时传入的packages实际上就是我们前面在ReactInstance中分析的reactPackages

DefaultTurboModuleManagerDelegate实际上没实现getEagerInitModuleNames,我们来看父类的实现:

// ReactPackageTurboModuleManagerDelegate.kt

  protected constructor(
      reactApplicationContext: ReactApplicationContext,
      packages: List<ReactPackage>,
      hybridData: HybridData,
  ) : super(hybridData) {
    initialize(reactApplicationContext, packages)
  }

  override fun getEagerInitModuleNames(): List<String> = buildList {
    for (moduleProvider in moduleProviders) {
      for (moduleInfo in packageModuleInfos[moduleProvider]?.values ?: emptyList()) {
        if (moduleInfo.isTurboModule && moduleInfo.needsEagerInit) {
          add(moduleInfo.name)
        }
      }
    }
  }

  private fun initialize(
        reactApplicationContext: ReactApplicationContext,
        packages: List<ReactPackage>,
    ) {
      val applicationContext: ReactApplicationContext = reactApplicationContext
      for (reactPackage in packages) {
       /**
         * BaseReactPackage(新架构,推荐方式)
         * 
         * BaseReactPackage 是专为新架构设计的抽象类,特点:
         * - 支持懒加载:模块只在需要时才创建(通过 getModule(name))
         * - 提供模块元信息:通过 getReactModuleInfoProvider() 获取预定义的模块信息
         * - 性能优化:避免启动时创建所有模块,减少内存占用和启动时间
         */
        if (reactPackage is BaseReactPackage) {
          val moduleProvider = ModuleProvider { moduleName: String ->
            reactPackage.getModule(moduleName, applicationContext)
          }
          moduleProviders.add(moduleProvider)
          packageModuleInfos[moduleProvider] =
              reactPackage.getReactModuleInfoProvider().getReactModuleInfos()
          continue
        }
                // 省略过时的旧架构代码......
      }
    }

通过对以上代码分析,流程就很清晰了。它在ReactPackageTurboModuleManagerDelegate的构造方法中调用initialize方法进行初始化。

核心职责是:

  • 遍历所有 ReactPackage,为每个包创建 ModuleProvider(模块提供者),用于按需创建 NativeModule
  • 收集并缓存所有模块的元信息(ReactModuleInfo),包括模块名、类型、是否 TurboModule 等

初始化完成后,那么接下来的getEagerInitModuleNames调用就很好理解,主要就是返回所有标记为 needsEagerInit = true 的 TurboModule 模块名列表,这些模块会在 ReactInstance 初始化后立即创建,而不是等到首次使用时才创建。

之所以需要预加载,是因为某些模块必须在应用启动时立即初始化,从而避免首次使用时创建模块导致的延迟和卡顿,同时确保关键基础设施模块在 JS bundle 加载前就准备好。

C++ 侧的初始化

现在我们来分析TurboModuleManager中的Native方法initHybrid。首先该类加载的动态库是SoLoader.loadLibrary("turbomodulejsijni"),所以对应的JNI实现,肯定位于turbomodulejsijni库中,其次上层Kotlin类和Native层C++类是一种映射关系,所以C++中也应该有一个对应类叫TurboModuleManager,这也是我们在启动流程里面分析过的,使用fbjni的原因。根据这两个条件,就能准确定位到此处initHybrid的具体实现(注意,全局搜索initHybrid会发现有很多同名方法的)。

头文件源码react-native/packages/react-native/ReactAndroid/src/main/jni/react/turbomodule/ReactCommon/TurboModuleManager.h

  static jni::local_ref<jhybriddata> initHybrid(
      jni::alias_ref<jhybridobject> /* unused */,
      jni::alias_ref<JRuntimeExecutor::javaobject> runtimeExecutor,
      jni::alias_ref<CallInvokerHolder::javaobject> jsCallInvokerHolder,
      jni::alias_ref<NativeMethodCallInvokerHolder::javaobject> nativeMethodCallInvokerHolder,
      jni::alias_ref<TurboModuleManagerDelegate::javaobject> delegate);

CPP源码react-native/packages/react-native/ReactAndroid/src/main/jni/react/turbomodule/ReactCommon/TurboModuleManager.cpp

TurboModuleManager::TurboModuleManager(
    RuntimeExecutor runtimeExecutor,
    std::shared_ptr<CallInvoker> jsCallInvoker,
    std::shared_ptr<NativeMethodCallInvoker> nativeMethodCallInvoker,
    jni::alias_ref<TurboModuleManagerDelegate::javaobject> delegate)
    : runtimeExecutor_(std::move(runtimeExecutor)),
      jsCallInvoker_(std::move(jsCallInvoker)),
      nativeMethodCallInvoker_(std::move(nativeMethodCallInvoker)),
      delegate_(jni::make_global(delegate)) {}

jni::local_ref<TurboModuleManager::jhybriddata> TurboModuleManager::initHybrid(
    jni::alias_ref<jhybridobject> /* unused */,
    jni::alias_ref<JRuntimeExecutor::javaobject> runtimeExecutor,
    jni::alias_ref<CallInvokerHolder::javaobject> jsCallInvokerHolder,
    jni::alias_ref<NativeMethodCallInvokerHolder::javaobject>
        nativeMethodCallInvokerHolder,
    jni::alias_ref<TurboModuleManagerDelegate::javaobject> delegate) {
  return makeCxxInstance(
      runtimeExecutor->cthis()->get(),
      jsCallInvokerHolder->cthis()->getCallInvoker(),
      nativeMethodCallInvokerHolder->cthis()->getNativeMethodCallInvoker(),
      delegate);
}

void TurboModuleManager::registerNatives() {
  registerHybrid({
      makeNativeMethod("initHybrid", TurboModuleManager::initHybrid),
      makeNativeMethod(
          "installJSIBindings", TurboModuleManager::installJSIBindings),
  });
}

这里大量使用了fbjni提供的机制,这对于熟悉和不熟悉JVM JNI机制的人都会造成一定程度混乱。首先我们理一理调用的流程,根据JNI的机制,当在Java类中加载动态库时,其动态库中的JNI_OnLoad就会被调用。这里有一份谷歌官方文档,详细介绍了JNI_OnLoad方法的使用,包括如何注册Native方法名等,JNI 文档。现在看到react-native/packages/react-native/ReactAndroid/src/main/jni/react/turbomodule/ReactCommon/OnLoad.cpp

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* /*unused*/) {
  return facebook::jni::initialize(vm, [] {
    // TODO: dvacca ramanpreet unify this with the way
    // "ComponentDescriptorFactory" is defined in Fabric
    facebook::react::TurboModuleManager::registerNatives();

    facebook::jni::registerNatives(
        "com/facebook/react/internal/turbomodule/core/TurboModulePerfLogger",
        {makeNativeMethod("jniEnableCppLogging", jniEnableCppLogging)});
  });
}

现在流程就很清晰了,当kotlin中SoLoader.loadLibrary("turbomodulejsijni")加载时,JNI_OnLoad被调用,TurboModuleManager中的静态函数registerNatives被调用,注册了一个Native方法initHybrid,并将其与TurboModuleManager的静态方法initHybrid进行映射。

现在,当Kotlin中的Native方法initHybrid调用时,就会调用TurboModuleManager的静态函数initHybrid。接下来,initHybrid的实现中,调用了fbjni提供的makeCxxInstance函数。其内部实现如下:

  static local_ref<detail::HybridData> makeHybridData(
      std::unique_ptr<T> cxxPart) {
    auto hybridData = detail::HybridData::create();
    setNativePointer(hybridData, std::move(cxxPart));
    return hybridData;
  }

  template <typename... Args>
  static local_ref<detail::HybridData> makeCxxInstance(Args&&... args) {
    return makeHybridData(
        std::unique_ptr<T>(new T(std::forward<Args>(args)...)));
  }

这是两个模版函数。其主要的功能就是做了三件事:

  1. 根据模版类型创建 C++ 对象
  2. 创建 HybridData 包装器来包装 C++ 对象
  3. 返回 JNI 本地引用。local_ref就是一个智能指针。

到这里C++的initHybrid就很清楚了,主要就是创建了一个C++的TurboModuleManager对象,并使用了 HybridData 包装返回。

该方法的几个参数:

参数 类型 作用
runtimeExecutor JRuntimeExecutor 在 JS 线程上执行代码的执行器
jsCallInvokerHolder CallInvokerHolder 持有从 C++ 调用 JS 的调用器
nativeMethodCallInvokerHolder NativeMethodCallInvokerHolder 持有从 JS 调用 Native 的调用器
delegate TurboModuleManagerDelegate 提供 TurboModule 实例的委托对象

接下来在Kotlin中调用的installJSIBindings方法,自然也是对应到C++类中的静态函数:

// TurboModuleManager.cpp 
void TurboModuleManager::installJSIBindings(
    jni::alias_ref<jhybridobject> javaPart,      // ← Java 端 TurboModuleManager 对象引用
    bool shouldCreateLegacyModules) {            // ← 是否创建旧架构模块的标志
  auto cxxPart = javaPart->cthis();              // ← 获取 C++ 端对象指针
  if (cxxPart == nullptr || !cxxPart->jsCallInvoker_) {
    return; // 连接到 Chrome 调试器时,运行时不存在。
  }

  cxxPart->runtimeExecutor_([javaPart = jni::make_global(javaPart),
                             shouldCreateLegacyModules](jsi::Runtime& runtime) {
    // 注意,此 lambda中已经线程切换了, 是在 JS 线程上执行的
    TurboModuleBinding::install(                 // ← 在 JS Runtime 中安装绑定
        runtime,
        createTurboModuleProvider(javaPart, &runtime),
        shouldCreateLegacyModules ? createLegacyModuleProvider(javaPart)
                                  : nullptr);
  });
}

TurboModuleProviderFunctionType TurboModuleManager::createTurboModuleProvider(
    jni::alias_ref<jhybridobject> javaPart,
    jsi::Runtime* runtime) {
  return [runtime, weakJavaPart = jni::make_weak(javaPart)](
             const std::string& name) -> std::shared_ptr<TurboModule> {
    auto javaPart = weakJavaPart.lockLocal();      // ← 尝试获取强引用
    if (!javaPart) {
      return nullptr;                              // ← Java 对象已被回收
    }

    auto cxxPart = javaPart->cthis();
    if (cxxPart == nullptr) {
      return nullptr;                              // ← C++ 对象已被销毁
    }

    return cxxPart->getTurboModule(javaPart, name, *runtime); // ← 获取模块实例
  };
}

可以看到,createTurboModuleProvider方法返回的是一个闭包,也就是C++中的lambda函数。此闭包的返回值是TurboModule类型,可见,如何查找调用TurboModule的核心逻辑,肯定就在cxxPart->getTurboModule(javaPart, name, *runtime)这行,这里的getTurboModule方法,就是当前C++ TurboModuleManager对象中的方法。此处我们暂时略过,放到下一节TurboModule 调用流程详细分析。

现在我们应该看看TurboModuleBinding::install函数做了什么。

源码react-native/packages/react-native/ReactCommon/react/nativemodule/core/ReactCommon/TurboModuleBinding.cpp

void TurboModuleBinding::install(
    jsi::Runtime& runtime,
    TurboModuleProviderFunctionType&& moduleProvider,
    TurboModuleProviderFunctionType&& legacyModuleProvider,
    std::shared_ptr<LongLivedObjectCollection> longLivedObjectCollection) {
  // TODO(T208105802): We can get this information from the native side!
  auto isBridgeless = runtime.global().hasProperty(runtime, "RN$Bridgeless");

  if (!isBridgeless) {
    runtime.global().setProperty(
    runtime,
    "__turboModuleProxy",
    // 旧架构省略......
    );
    return;
  }
  // 新架构(Bridgeless 模式),安装 nativeModuleProxy HostObject
  defineReadOnlyGlobal(
      runtime,
      "nativeModuleProxy",   // ← 全局对象名
      jsi::Object::createFromHostObject(
          runtime,
          std::make_shared<BridgelessNativeModuleProxy>(
              runtime,
              std::move(moduleProvider),
              std::move(legacyModuleProvider),
              longLivedObjectCollection)));
}

此处的defineReadOnlyGlobal函数就涉及到JSI接口调用了。JSI之于JS 引擎,就相当于JNI之于JVM。JSI就是打通上层JS代码与底层C++互相调用的引擎接口框架。对于JSI接口的详细分析,会再单独的篇章介绍。这里只需要知道,此方法在JS运行时定义了一个只读全局属性,此方法等价于以下JS代码:

Object.defineProperty(global, propName, {
  value: value,
  writable: false,        // ← 不可写(只读)
  enumerable: false,      // ← 不可枚举(隐藏)
  configurable: false     // ← 不可配置(不可删除/修改描述符)
});

那么这里,就是在JS全局定义了一个只读全局属性nativeModuleProxy,此变量的内部类型是BridgelessNativeModuleProxy。也就是说,上层JS使用nativeModuleProxy时,其实就是在调用底层C++的BridgelessNativeModuleProxy对象。

总的来说,TurboModuleBinding::install的作用就是创建了一个BridgelessNativeModuleProxy对象,其持有moduleProvider闭包,然后注册给上层JavaScript使用。此方法有个地方需要留意,在旧架构时,其定义的全局对象是__turboModuleProxy

TurboModule 调用流程

先回顾一下官方给出的编写TurboModule的规范:

import type {TurboModule} from 'react-native';
import {TurboModuleRegistry} from 'react-native';

export interface Spec extends TurboModule {
  setItem(value: string, key: string): void;
  getItem(key: string): string | null;
}

export default TurboModuleRegistry.getEnforcing<Spec>(
  'NativeLocalStorage',
) as Spec;

第一步是编写一个继承自TurboModule的接口,描述我们想要定义的方法。最后是通过export default导出了一个Spec类型的实现对象。外部使用此模块的代码,只需要导入这个对象就可以直接调用我们接口声明的这些方法。现在的关键是分析一下getEnforcing方法做了些什么。

查看源码react-native/packages/react-native/Libraries/TurboModule/TurboModuleRegistry.js

import type {TurboModule} from './RCTExport';
import invariant from 'invariant';

const NativeModules = require('../BatchedBridge/NativeModules').default;
const turboModuleProxy = global.__turboModuleProxy;

function requireModule<T: TurboModule>(name: string): ?T {
  if (turboModuleProxy != null) {
    const module: ?T = turboModuleProxy(name);
    if (module != null) {
      return module;
    }
  }

  const legacyModule: ?T = NativeModules[name];
  if (legacyModule != null) {
    return legacyModule;
  }

  return null;
}

export function get<T: TurboModule>(name: string): ?T {
  return requireModule<T>(name);
}

export function getEnforcing<T: TurboModule>(name: string): T {
  const module = requireModule<T>(name);
  invariant(
    module != null,
    `TurboModuleRegistry.getEnforcing(...): '${name}' could not be found. ` +
      'Verify that a module by this name is registered in the native binary.',
  );
  return module;
}

getEnforcing方法实际上是调用的requireModule来查找模块,requireModule方法实现需要注意,其中turboModuleProxy是旧架构的机制。这点我们在前面TurboModuleBinding::install方法分析时就知道了。但此处的实现代码仍然具有误导性,既然turboModuleProxy在新架构不存在,那么就应该执行const legacyModule: ?T = NativeModules[name]这行来查找模块,但其变量命名看,是加载旧架构模块。实际上此处是兼容代码,同时兼容新旧架构。想要洞悉其中玄机,我们需要阅读react-native/packages/react-native/Libraries/BatchedBridge/NativeModules.js的实现源码:

let NativeModules: {[moduleName: string]: any, ...} = {};
if (global.nativeModuleProxy) {
  NativeModules = global.nativeModuleProxy;
} else {
  // 旧架构代码省略......
}

export default NativeModules;

可以看到,这里NativeModules变量的声明相当于一个三目表达式,当global.nativeModuleProxy属性存在时,直接返回nativeModuleProxy对象,否则加载旧架构的模块。

到此就豁然开朗了,TurboModuleRegistry.getEnforcing<Spec>( 'NativeLocalStorage')调用实际上就等价于global.nativeModuleProxy['NativeLocalStorage']

我们已经知道nativeModuleProxy实际上就是C++类BridgelessNativeModuleProxy的实例,其也定义在TurboModuleBinding.cpp中:

// TurboModuleBinding.cpp
class BridgelessNativeModuleProxy : public jsi::HostObject {
  TurboModuleBinding turboBinding_;
  std::unique_ptr<TurboModuleBinding> legacyBinding_;

 public:
  BridgelessNativeModuleProxy(
      jsi::Runtime& runtime,
      TurboModuleProviderFunctionType&& moduleProvider,
      TurboModuleProviderFunctionType&& legacyModuleProvider,
      std::shared_ptr<LongLivedObjectCollection> longLivedObjectCollection)
      : turboBinding_(
            runtime,
            std::move(moduleProvider),
            longLivedObjectCollection),
        legacyBinding_(
            legacyModuleProvider ? std::make_unique<TurboModuleBinding>(
                                       runtime,
                                       std::move(legacyModuleProvider),
                                       longLivedObjectCollection)
                                 : nullptr) {}

  jsi::Value get(jsi::Runtime& runtime, const jsi::PropNameID& name) override {
    /**
     * BatchedBridge/NativeModules.js 包含这一行:
     *
     * module.exports = global.nativeModuleProxy
     *
     * 这意味着 NativeModuleProxy 作为模块从 'NativeModules.js' 导出。
     * 每当某些 JavaScript 需要 'NativeModule.js' 时,
     * Metro 检查此模块的 __esModule 属性以查看模块是否为 ES6 模块。
     *
     * 我们从此属性访问返回 false,这样我们可以在稍后发生的实际 
     * NativeModule require 上失败,这更具可操作性。
     */
    std::string moduleName = name.utf8(runtime);
    if (moduleName == "__esModule") {
      return {false}; // 表示不是 ES6 模块
    }

    // 优先尝试从 TurboModule 系统获取模块
    auto turboModule = turboBinding_.getModule(runtime, moduleName);
    if (turboModule.isObject()) {
      return turboModule;
    }

    // 回退到 Legacy Module 系统(兼容旧架构)
    if (legacyBinding_) {
      auto legacyModule = legacyBinding_->getModule(runtime, moduleName);
      if (legacyModule.isObject()) {
        return legacyModule;
      }
    }
    // 模块不存在,返回 null
    return jsi::Value::null();
  }
  // 省略……
};

这里继承自JSI接口提供的jsi::HostObject类型,这表示此C++类是可以直接被上层JS代码使用的对象类型。根据JSI的要求,如果一个C++类希望被JS直接调用,那么就要继承jsi::HostObject

此类首先创建了一个TurboModuleBinding对象,当上层调用global.nativeModuleProxy['NativeLocalStorage']时,就会调用其get方法,作为新架构,接下来就会调用TurboModuleBindinggetModule来查找模块。

// TurboModuleBinding.cpp
jsi::Value TurboModuleBinding::getModule(
    jsi::Runtime& runtime,
    const std::string& moduleName) const {
  std::shared_ptr<TurboModule> module;
  {
    TraceSection s("TurboModuleBinding::moduleProvider", "module", moduleName);
    module = moduleProvider_(moduleName);  // ← 调用moduleProvider获取模块
  }
  if (module) {
    TurboModuleWithJSIBindings::installJSIBindings(module, runtime);

    // jsRepresentation 是什么?TurboModule 属性的缓存
    // 此后,始终将缓存(即:jsRepresentation)返回给 JavaScript
    //
    // 如果在 TurboModule 上找到 jsRepresentation,则返回它。
    //
    // 注意:TurboModule 在 TurboModuleManager 中按名称缓存。因此,
    // jsRepresentation 也由 TurboModuleManager 按名称缓存
    auto& weakJsRepresentation = module->jsRepresentation_;
    if (weakJsRepresentation) {
      auto jsRepresentation = weakJsRepresentation->lock(runtime);
      if (!jsRepresentation.isUndefined()) {
        return jsRepresentation;  // ← 返回缓存的 JS 对象
      }
    }

    // 状态:在 TurboModule 上未找到 jsRepresentation
    // 创建一个全新的 jsRepresentation,并将其附加到 TurboModule
    jsi::Object jsRepresentation(runtime);
    weakJsRepresentation =
        std::make_unique<jsi::WeakObject>(runtime, jsRepresentation);

    // 在属性访问时延迟填充 jsRepresentation。
    //
    // 这是如何工作的?
    //   1. 最初 jsRepresentation 是空的:{}
    //   2. 如果在 jsRepresentation 上的属性查找失败,JS 运行时将
    //   搜索 jsRepresentation 的原型:jsi::Object(TurboModule)。
    //   3. TurboModule::get(runtime, propKey) 执行。这会创建
    //   属性,将其缓存在 jsRepresentation 上,然后将其返回给JavaScript
    auto hostObject =
        jsi::Object::createFromHostObject(runtime, std::move(module));
    jsRepresentation.setProperty(runtime, "__proto__", std::move(hostObject));

    return jsRepresentation; 
  } else {
    return jsi::Value::null();
  }
}

这里moduleProvider_正是我们前面略过的内容,在TurboModuleManager::createTurboModuleProvider方法中创建的一个闭包。此时传入模块名调用此闭包,闭包中主要还是调用TurboModuleManager::getTurboModule方法:

// TurboModuleManager.cpp

std::shared_ptr<TurboModule> TurboModuleManager::getTurboModule(
    jni::alias_ref<jhybridobject> javaPart,
    const std::string& name,
    jsi::Runtime& runtime) {
  const char* moduleName = name.c_str();
  TurboModulePerfLogger::moduleJSRequireBeginningStart(moduleName);

  // 1. 检查 C++ 缓存
  auto turboModuleLookup = turboModuleCache_.find(name);
  if (turboModuleLookup != turboModuleCache_.end()) {
    TurboModulePerfLogger::moduleJSRequireBeginningCacheHit(moduleName);
    TurboModulePerfLogger::moduleJSRequireBeginningEnd(moduleName);
    return turboModuleLookup->second;  // ← 缓存命中,直接返回
  }

  TurboModulePerfLogger::moduleJSRequireBeginningEnd(moduleName);

  // 2. 尝试获取 C++ TurboModule(通过 delegate)
  auto cxxDelegate = delegate_->cthis();
  auto cxxModule = cxxDelegate->getTurboModule(name, jsCallInvoker_);
  if (cxxModule) {
    turboModuleCache_.insert({name, cxxModule});  // 加入C++缓存
    return cxxModule;
  }

  // 3. 尝试获取全局注册的 C++ TurboModule
  auto& cxxTurboModuleMapProvider = globalExportedCxxTurboModuleMap();
  auto it = cxxTurboModuleMapProvider.find(name);
  if (it != cxxTurboModuleMapProvider.end()) {
    auto turboModule = it->second(jsCallInvoker_);
    turboModuleCache_.insert({name, turboModule});
    return turboModule;
  }

  // 4. 调用 Java 层获取 Java TurboModule
  static auto getTurboJavaModule = javaPart->getClass()
                                                                                            ->getMethod<jni::alias_ref<JTurboModule>(const std::string&)>(
                                                                                                                                                                              "getTurboJavaModule");
  auto moduleInstance = getTurboJavaModule(javaPart.get(), name);
  if (moduleInstance) {
    TurboModulePerfLogger::moduleJSRequireEndingStart(moduleName);
    // 5. 创建 JavaTurboModule 包装
    JavaTurboModule::InitParams params = {
        .moduleName = name,
        .instance = moduleInstance,
        .jsInvoker = jsCallInvoker_,
        .nativeMethodCallInvoker = nativeMethodCallInvoker_};

    auto turboModule = cxxDelegate->getTurboModule(name, params);
    // 6. 处理 JSI Bindings(如果有)
    if (moduleInstance->isInstanceOf(JTurboModuleWithJSIBindings::javaClassStatic())) {
      static auto getBindingsInstaller =
          JTurboModuleWithJSIBindings::javaClassStatic()
              ->getMethod<BindingsInstallerHolder::javaobject()>(
                  "getBindingsInstaller");
      auto installer = getBindingsInstaller(moduleInstance);
      if (installer) {
        installer->cthis()->installBindings(runtime, jsCallInvoker_);
      }
    }

    turboModuleCache_.insert({name, turboModule});
    TurboModulePerfLogger::moduleJSRequireEndingEnd(moduleName);
    return turboModule;
  }

  // 省略Legacy Module ......

  return nullptr;
}

查找优先级

  1. C++ 缓存turboModuleCache_
  2. C++ TurboModule:通过 delegate
  3. 全局 C++ TurboModuleglobalExportedCxxTurboModuleMap()
  4. Java TurboModule:首先通过 JNI 调用 getTurboJavaModule(),接着调用C++层的getTurboModule
  5. Legacy C++ Module:兼容旧架构

C++ TurboModule

需要注意,React Native的TurboModule是分为两种的,一种是纯C++ TurboModule,另一种是基于原生的TurboModule。原生的TurboModule与平台相关,在Android平台使用Java/Kotlin 语言开发,在iOS上主要是ObjC/Swift开发。而纯C++ TurboModule 就是C++开发,开发一次就可以同时兼容两大平台。因此建议将一些通用的代码可以用纯C++开发,减少两份代码的维护成本。

此处逻辑很清晰,优先查找第三方注册的纯C++ TurboModule,其次找全局注册的C++ TurboModule,最后找Java开发的TurboModule(安卓平台Kotlin最终也是编译成Java字节码,等价于Java)。

先来分析一下C++ TurboModule的查找逻辑,这里的cxxDelegateTurboModuleManagerDelegate类型,其具体实现类是DefaultTurboModuleManagerDelegate

源码react-native/packages/react-native/ReactAndroid/src/main/jni/react/newarchdefaults/DefaultTurboModuleManagerDelegate.cpp

std::shared_ptr<TurboModule> DefaultTurboModuleManagerDelegate::getTurboModule(
    const std::string& name,
    const std::shared_ptr<CallInvoker>& jsInvoker) {
  // 1. 遍历所有注册的 CxxReactPackage
  for (const auto& cxxReactPackage : cxxReactPackages_) {
    auto cppPart = cxxReactPackage->cthis();
    if (cppPart != nullptr) {
      auto module = cppPart->getModule(name, jsInvoker);
      if (module) {
        return module;
      }
    }
  }

  // 2. 查找全局ModuleProvider
  auto moduleProvider = DefaultTurboModuleManagerDelegate::cxxModuleProvider;
  if (moduleProvider) {
    auto module = moduleProvider(name, jsInvoker);
    if (module) {
      return module;
    }
  }

  // 3. 查找默认系统模块
  return DefaultTurboModules::getTurboModule(name, jsInvoker);
}

可以看到也是三级查找,首先遍历cxxReactPackages_,此值实际上是RN框架在初始化时传入的,具体可以回顾一下安卓初始化流程分析一文,实际上应该是空列表,因为一般也是在应用的Application处注册,如下cxxReactPackageProviders

  public fun getDefaultReactHost(
      context: Context,
      packageList: List<ReactPackage>,
      jsMainModulePath: String = "index",
      jsBundleAssetPath: String = "index.android.bundle",
      jsBundleFilePath: String? = null,
      jsRuntimeFactory: JSRuntimeFactory? = null,
      useDevSupport: Boolean = ReactBuildConfig.DEBUG,
      cxxReactPackageProviders: List<(ReactContext) -> CxxReactPackage> = emptyList(),
      exceptionHandler: (Exception) -> Unit = { throw it },
      bindingsInstaller: BindingsInstaller? = null,
  ): ReactHost {
    //......
  }

DefaultTurboModules::getTurboModule是查找的是RN框架内部定义的C++ TurboModule,重点需要关注一下DefaultTurboModuleManagerDelegate::cxxModuleProvider,这里是查找第三方的C++ TurboModule。

// DefaultTurboModuleManagerDelegate.cpp
std::function<std::shared_ptr<TurboModule>(
    const std::string&,
    const std::shared_ptr<CallInvoker>&)>
    DefaultTurboModuleManagerDelegate::cxxModuleProvider{nullptr};

可以看到cxxModuleProvider实际上是一个全局变量,只不过是一个函数类型的变量。那么此变量是在哪里赋值的呢?通过全局搜索,只有一处进行了设置,源码react-native/packages/react-native/ReactAndroid/cmake-utils/default-app-setup/OnLoad.cpp

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) {
  return facebook::jni::initialize(vm, [] {
    facebook::react::DefaultTurboModuleManagerDelegate::cxxModuleProvider =
        &facebook::react::cxxModuleProvider;
    facebook::react::DefaultTurboModuleManagerDelegate::javaModuleProvider =
        &facebook::react::javaModuleProvider;
    facebook::react::DefaultComponentsRegistry::
        registerComponentDescriptorsFromEntryPoint =
            &facebook::react::registerComponents;
  });
}

std::shared_ptr<TurboModule> cxxModuleProvider(
    const std::string& name,
    const std::shared_ptr<CallInvoker>& jsInvoker) {
  // Here you can provide your CXX Turbo Modules coming from
  // either your application or from external libraries. The approach to follow
  // is similar to the following (for a module called `NativeCxxModuleExample`):
  //
  // if (name == NativeCxxModuleExample::kModuleName) {
  //   return std::make_shared<NativeCxxModuleExample>(jsInvoker);
  // }

  // And we fallback to the CXX module providers autolinked
  return autolinking_cxxModuleProvider(name, jsInvoker);

  return nullptr;
}

这里其实就是在libappmodules.so加载时进行了初始化,最终是调用的autolinking_cxxModuleProvider来自动完成三方库的C++ TurboModule注册。毫无疑问,肯定又是基于codegen工具进行了自动代码生成,具体代码生成逻辑,放到后面再研究。但是查找逻辑我们已经非常清晰,最终就是创建了类似NativeCxxModuleExample这样的纯C++三方TurboModule对象。

Java TurboModule

接下来再看另一条线Java TurboModule的查找,可以分为两步,首先是Java层的调用,将返回的TurboModule实例封装到InitParams结构中作为参数,再次调用C++层的getTurboModule

// TurboModuleManager.cpp

static auto getTurboJavaModule = javaPart->getClass()->
                                          getMethod<jni::alias_ref<JTurboModule>(const std::string&)>("getTurboJavaModule");
// Java层的调用
auto moduleInstance = getTurboJavaModule(javaPart.get(), name);
if (moduleInstance) {
  TurboModulePerfLogger::moduleJSRequireEndingStart(moduleName);
  JavaTurboModule::InitParams params = {
      .moduleName = name,
      .instance = moduleInstance,
      .jsInvoker = jsCallInvoker_,
      .nativeMethodCallInvoker = nativeMethodCallInvoker_};
  // C++层的调用
  auto turboModule = cxxDelegate->getTurboModule(name, params);
  // 省略......
  return turboModule;
}

Java层是通过JNI反射并调用kotlin类TurboModuleManager中的getTurboJavaModule方法:

// TurboModuleManager.kt

@Suppress("unused")
@DoNotStrip
private fun getTurboJavaModule(moduleName: String): TurboModule? {
  /*
   * 这个 API 从 global.__turboModuleProxy 调用
   * 只有当 native 模块是 turbo 模块时才调用 getModule
   */
  if (!isTurboModule(moduleName)) {
    return null
  }

  val module = getModule(moduleName)
  return if (module !is CxxModuleWrapper && module is TurboModule) module else null
}

/**
 * 返回与提供的 moduleName 对应的 NativeModule 实例。
 *
 * 此方法:
 * - 如果模块尚不存在,则创建并初始化该模块
 * - 在 TurboModuleManager 被销毁后返回 null
 */
override fun getModule(moduleName: String): NativeModule? {
  val moduleHolder: ModuleHolder?

  synchronized(moduleCleanupLock) {
    if (moduleCleanupStarted) {   // 检查标志,判断 TurboModuleManager 是否正在清理(调用了 invalidate())
      /*
       * 清理开始后总是返回 null,这样 getNativeModule(moduleName) 返回 null
       */
      FLog.e(
          TAG,
          "getModule(): Tried to get module \"%s\", but TurboModuleManager was tearing down (legacy: %b, turbo: %b)",
          moduleName,
          isLegacyModule(moduleName),
          isTurboModule(moduleName),
      )
      return null   // 回 null,拒绝创建新模块
    }
    /*
     * TODO(T64619790): 我们是否应该提前填充 moduleHolders,以避免必须控制对它的并发访问?
     */
    if (!moduleHolders.containsKey(moduleName)) {
      moduleHolders[moduleName] = ModuleHolder()
    }
    moduleHolder = moduleHolders[moduleName]
  }

  if (moduleHolder == null) {
    FLog.e(TAG, "getModule(): Tried to get module \"%s\", but moduleHolder was null", moduleName)
    return null
  }

  TurboModulePerfLogger.moduleCreateStart(moduleName, moduleHolder.moduleId)
  // 获取或创建模块
  val module = getOrCreateModule(moduleName, moduleHolder, true)

  if (module != null) {
    TurboModulePerfLogger.moduleCreateEnd(moduleName, moduleHolder.moduleId)
  } else {
    TurboModulePerfLogger.moduleCreateFail(moduleName, moduleHolder.moduleId)
  }

  return module
}

/**
 * 给定一个 ModuleHolder 和 TurboModule 的 moduleName,返回 TurboModule 实例。
 *
 * 使用 ModuleHolder 来确保如果 n 个线程竞争创建 TurboModule x,那么只有
 * 第一个线程创建 x。其他 n - 1 个线程会等待,直到 x 被创建并初始化完成。
 */
private fun getOrCreateModule(
    moduleName: String,
    moduleHolder: ModuleHolder,
    shouldPerfLog: Boolean,
): NativeModule? {
  var shouldCreateModule = false

  synchronized(moduleHolder) {
    if (moduleHolder.isDoneCreatingModule) {
      if (shouldPerfLog) {
        TurboModulePerfLogger.moduleCreateCacheHit(moduleName, moduleHolder.moduleId)
      }

      return moduleHolder.module
    }
    if (!moduleHolder.isCreatingModule) {
      // 只有一个线程能到达这里。
      shouldCreateModule = true
      moduleHolder.startCreatingModule()
    }
  }

  if (shouldCreateModule) {
    TurboModulePerfLogger.moduleCreateConstructStart(moduleName, moduleHolder.moduleId)
    var nativeModule = turboModuleProvider.getModule(moduleName)

    if (nativeModule == null) {
      // 回退到 Legacy 
      nativeModule = legacyModuleProvider.getModule(moduleName)
    }

    TurboModulePerfLogger.moduleCreateConstructEnd(moduleName, moduleHolder.moduleId)
    TurboModulePerfLogger.moduleCreateSetUpStart(moduleName, moduleHolder.moduleId)

    if (nativeModule != null) {
      synchronized(moduleHolder) { moduleHolder.module = nativeModule }

      /*
       * TurboModuleManager 在 ReactApplicationContext 设置完成后初始化
       * NativeModules 应该在 ReactApplicationContext 设置完成后初始化
       * 因此,我们现在应该初始化 TurboModule
       */
      nativeModule.initialize()
    } else {
      FLog.e(
          TAG,
          "getOrCreateModule(): Unable to create module \"%s\" (legacy: %b, turbo: %b)",
          moduleName,
          isLegacyModule(moduleName),
          isTurboModule(moduleName),
      )
    }

    // 标记创建完成并唤醒等待线程
    TurboModulePerfLogger.moduleCreateSetUpEnd(moduleName, moduleHolder.moduleId)
    synchronized(moduleHolder) {
      moduleHolder.endCreatingModule()
      (moduleHolder as Object).notifyAll()
    }

    return nativeModule
  }

  synchronized(moduleHolder) {
    var wasInterrupted = false
    while (moduleHolder.isCreatingModule) {
      try {
        // 等待直到 TurboModule 被创建和初始化
        (moduleHolder as Object).wait()
      } catch (e: InterruptedException) {
        wasInterrupted = true
      }
    }

    if (wasInterrupted) {
      /*
       * TurboModules 理想情况下应该快速创建和初始化。因此,
       * 我们等到 TurboModule 完成初始化后再重新中断当前线程。
       */
      Thread.currentThread().interrupt()
    }
    return moduleHolder.module
  }
}

代码不难理解,我们概括一下流程:

┌──────────────────────────────────────────────────────────┐
│          第一层:getTurboJavaModule                       │
├──────────────────────────────────────────────────────────┤
│  核心步骤:                                               │
│  1. 检查是否是 TurboModule(类型验证)                    │
│  2. 调用 getModule 获取模块                               │
│  3. 过滤返回:只返回 Java TurboModule                     │
└──────────────────────────────────────────────────────────┘
                      ↓
┌──────────────────────────────────────────────────────────┐
│              第二层:getModule                            │
├──────────────────────────────────────────────────────────┤
│  核心步骤:                                               │
│  1. 检查清理状态(拒绝创建)                              │
│  2. 获取或创建 ModuleHolder(模块持有者)                 │
│  3. 调用 getOrCreateModule 执行实际创建                   │
│  4. 记录性能日志                                          │
└──────────────────────────────────────────────────────────┘
                      ↓
┌──────────────────────────────────────────────────────────┐
│          第三层:getOrCreateModule                        │
├──────────────────────────────────────────────────────────┤
│  核心步骤:                                               │
│  1. 竞争检测:检查缓存或标记创建                          │
│  2. 创建逻辑(胜出线程):                                │
│     - 调用提供者创建模块实例                              │
│     - 初始化模块                                          │
│     - 唤醒等待线程                                        │
│  3. 等待逻辑(其他线程):                                │
│     - 等待创建完成                                        │
│     - 返回缓存的模块                                      │
└──────────────────────────────────────────────────────────┘
                      ↓
            返回 TurboModule 实例

最后,再来看一下turboModuleProvider.getModule方法的实现。在前面TurboModuleManager初始化一节我们已经知道了turboModuleProvider实际上是个闭包,内部其实就是调用的TurboModuleManagerDelegate类型的getModule方法返回Module。其具体实现则是在其子类ReactPackageTurboModuleManagerDelegate中:

// ReactPackageTurboModuleManagerDelegate.kt
  override fun getModule(moduleName: String): TurboModule? {
    var resolvedModule: NativeModule? = null

    for (moduleProvider in moduleProviders) {
      val moduleInfo: ReactModuleInfo? = packageModuleInfos[moduleProvider]?.get(moduleName)
      if (
          moduleInfo?.isTurboModule == true &&
              (resolvedModule == null || moduleInfo.canOverrideExistingModule)
      ) {
        val module = moduleProvider.getModule(moduleName)
        if (module != null) {
          resolvedModule = module
        }
      }
    }

    // 跳过与 TurboModule 不兼容的模块
    val isLegacyModule = resolvedModule !is TurboModule
    if (isLegacyModule) {
      return null
    }

    return resolvedModule as TurboModule
  }

现在再看C++层的调用:

// DefaultTurboModuleManagerDelegate.cpp
std::shared_ptr<TurboModule> DefaultTurboModuleManagerDelegate::getTurboModule(
    const std::string& name,
    const JavaTurboModule::InitParams& params) {
  auto moduleProvider = DefaultTurboModuleManagerDelegate::javaModuleProvider;
  if (moduleProvider) {
    if (auto resolvedModule = moduleProvider(name, params)) {
      return resolvedModule;
    }
  }

  return nullptr;
}

javaModuleProvider是一个全局变量,跟前面分析C++ TurboModule的cxxModuleProvider一样,都在同一个地方赋值:

// OnLoad.cpp
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) {
  return facebook::jni::initialize(vm, [] {
    facebook::react::DefaultTurboModuleManagerDelegate::cxxModuleProvider =
        &facebook::react::cxxModuleProvider;
    facebook::react::DefaultTurboModuleManagerDelegate::javaModuleProvider =
        &facebook::react::javaModuleProvider;
    // 省略......
  });
}

std::shared_ptr<TurboModule> javaModuleProvider(
    const std::string& name,
    const JavaTurboModule::InitParams& params) {
  // 提供自己的模块提供者。 遵循的方法类似于以下示例(对于名为 `samplelibrary`的库):
  //
  // auto module = samplelibrary_ModuleProvider(name, params);
  // if (module != nullptr) {
  //    return module;
  // }
  // return rncore_ModuleProvider(name, params);

  // 链接应用程序本地模块(如果可用)
#ifdef REACT_NATIVE_APP_MODULE_PROVIDER
  auto module = REACT_NATIVE_APP_MODULE_PROVIDER(name, params);
  if (module != nullptr) {
    return module;
  }
#endif

  // 首先尝试查找 React Native 核心模块
  if (auto module = FBReactNativeSpec_ModuleProvider(name, params)) {
    return module;
  }

  // 回退到自动链接的模块提供者
  if (auto module = autolinking_ModuleProvider(name, params)) {
    return module;
  }

  return nullptr;
}

这里三方库依然是走的自动链接函数autolinking_ModuleProvider。显然这也是通过Codegen自动生成的代码,我们放到后面再一起研究。

基本到这里,结合我们前面对moduleProviders初始化的分析,整套安卓的TurboModule初始化及其调用流程都已经通了。剩下的,就是React Native是如何扫描并自动将项目依赖的第三方TurboModule进行收集注册。

TurboModule 注册

Autolinking 机制剖析

Autolinking 机制在安卓平台是基于Gradle 插件系统来实现自动处理的。

Android Gradle 构建机制

简要介绍一下Android Gradle 构建机制原理: Gradle 构建生命周期

┌─────────────────────────────────────────────────────────────────┐
│ 1. Initialization 阶段                                          │
│    - 确定参与构建的项目                                          │
│    - 为每个项目创建 Project 实例                                 │
├─────────────────────────────────────────────────────────────────┤
│ 2. Settings 阶段 ← ReactSettingsPlugin 在这里执行               │
│    - 解析 settings.gradle(.kts)                                │
│    - 应用 Settings Plugin                                       │
│    - 执行 autolinkLibrariesFromCommand()                       │
│    - 生成 autolinking.json                                     │
│    - 动态添加子项目 (settings.include)                          │
├─────────────────────────────────────────────────────────────────┤
│ 3. Configuration 阶段 ← ReactPlugin 在这里执行                  │
│    - 解析所有 build.gradle(.kts)                               │
│    - 应用 Project Plugin                                        │
│    - 注册任务 (configureAutolinking)                           │
│    - 创建任务图(Task Graph)                                   │
├─────────────────────────────────────────────────────────────────┤
│ 4. Execution 阶段 ← GeneratePackageListTask 在这里执行          │
│    - 执行任务                                                   │
│    - 生成构建产物                                               │
└─────────────────────────────────────────────────────────────────┘

Settings Plugin 的特点:

  • 项目结构控制:可以动态决定哪些项目参与构建

  • 早期介入:在任何 Project Plugin 执行前就完成工作

  • 全局视角:能够访问整个构建的 Settings 对象

  • 依赖发现:适合做自动依赖发现和链接

ReactSettingsPlugin

ReactSettingsPlugin 是 Autolinking 机制的核心入口点,它通过 Gradle Settings Plugin 机制在项目配置的最早阶段介入,完成三方库的自动发现和链接。

ReactSettingsPlugin 架构层次:

┌─────────────────────────────────────────────────────────────────┐
│ Gradle Settings Plugin 层                                       │
├─────────────────────────────────────────────────────────────────┤
│ ReactSettingsPlugin (插件入口)                                  │
│   ↓                                                             │
│ ReactSettingsExtension (功能实现)                              │
│   ↓                                                             │
│ autolinkLibrariesFromCommand() (核心方法)                      │
└─────────────────────────────────────────────────────────────────┘
插件注册

源码react-native/packages/gradle-plugin/settings-plugin/build.gradle.kts

gradlePlugin {
  plugins {
    create("react.settings") {
      id = "com.facebook.react.settings"                    // ← 插件 ID
      implementationClass = "com.facebook.react.ReactSettingsPlugin"  // ← 实现类
    }
  }
}
插件核心实现

源码react-native/packages/gradle-plugin/settings-plugin/src/main/kotlin/com/facebook/react/ReactSettingsPlugin.kt

class ReactSettingsPlugin : Plugin<Settings> {
  override fun apply(settings: Settings) {
    // 关键:注册 ReactSettingsExtension 扩展
    settings.extensions.create(
        "reactSettings",                    // ← 扩展名称
        ReactSettingsExtension::class.java, // ← 扩展实现类
        settings                            // ← 注入 Settings 实例
    )
  }
}
  • Settings Plugin:在 Gradle Settings 阶段执行,早于所有 Project Plugin
  • Extension 注册:创建 reactSettings 扩展,供 settings.gradle 调用
  • 依赖注入:将 Settings 实例注入到 ReactSettingsExtension

继续查看实现类react-native/packages/gradle-plugin/settings-plugin/src/main/kotlin/com/facebook/react/ReactSettingsExtension.kt

abstract class ReactSettingsExtension @Inject constructor(val settings: Settings) {

  // 输出文件路径
  private val outputFile =
      settings.layout.rootDirectory.file("build/generated/autolinking/autolinking.json").asFile

  // 缓存目录
  private val outputFolder =
      settings.layout.rootDirectory.file("build/generated/autolinking/").asFile

  private val defaultConfigCommand: List<String> =
      windowsAwareCommandLine(listOf("npx", "@react-native-community/", "config")).map {
        it.toString()
      }

  /**
   * 使用外部命令作为权威数据,从而实现自动链接库的工具函数。
   *
   * 此方法应在 `settings.gradle` 文件中调用,它会确保 Gradle 项目加载所有发现的库。
   *
   * @param command 要执行的命令,用于获取自动链接配置。默认为
   *   `npx @react-native-community/cli config`。
   * @param workingDirectory 执行命令的工作目录。
   * @param lockFiles 要检查变化的锁文件列表(如果锁文件没有变化,则不会执行命令)。
   */
  @JvmOverloads
  public fun autolinkLibrariesFromCommand(
      command: List<String> = defaultConfigCommand,
      workingDirectory: File? = settings.layout.rootDirectory.dir("../").asFile,
      lockFiles: FileCollection =
          settings.layout.rootDirectory
              .dir("../")
              .files("yarn.lock", "package-lock.json", "package.json", "react-native.config.js"),
  ) {
    // 第一步:创建输出目录
    outputFile.parentFile.mkdirs()

    // 第二步:创建命令执行配置
    val updateConfig =
        object : GenerateConfig {
          override fun command(): List<String> = command

          override fun execute(): Int {
            // 执行 React Native CLI 命令
            val execResult = settings.providers.exec { exec ->
                  exec.commandLine(command)  // ← 执行 npx @react-native-community/cli config
                  exec.workingDir = workingDirectory  // ← 设置工作目录
                }
            // 将 CLI 输出写入 autolinking.json
            outputFile.writeText(execResult.standardOutput.asText.get())
            return execResult.result.get().exitValue
          }
        }

    // 第三步:检查缓存并更新配置
    checkAndUpdateCache(updateConfig, outputFile, outputFolder, lockFiles)

    // 第四步:链接发现的库
    linkLibraries(getLibrariesToAutolink(outputFile))
  }
  // 省略……
}

检查缓存并更新配置主要是通过SHA计算和检查锁文件实现。这里我们重点研究一下第四步链接发现的库:

// 提取可链接的库
internal fun getLibrariesToAutolink(buildFile: File): Map<String, File> {
    val model = JsonUtils.fromAutolinkingConfigJson(buildFile)  // ← 解析 JSON 配置
    return model
        ?.dependencies
        ?.values
        // 过滤:只处理有 Android 配置的依赖
        ?.filter { it.platforms?.android?.sourceDir != null }
        // 过滤:跳过纯 C++ 依赖(没有 .gradle 文件)
        ?.filterNot { it.platforms?.android?.isPureCxxDependency == true }
        // 映射:项目名 -> 源码目录
        ?.associate { deps ->
            ":${deps.nameCleansed}" to File(deps.platforms?.android?.sourceDir)
        } ?: emptyMap()
}

private fun linkLibraries(input: Map<String, File>) {
    input.forEach { (path, projectDir) ->
        settings.include(path)                    // ← 包含子项目到 Gradle 构建
        settings.project(path).projectDir = projectDir  // ← 设置项目目录
    }
}

可以看到,链接库时用到了之前注入的Settings实例,将依赖的那些三方库一个个添加到宿主工程中。

插件的引入

我们以React Native源码工程提供的helloworld项目为例,看一下这个gradle插件是怎么被引入的,源码react-native/private/helloworld/android/settings.gradle

pluginManagement { 
    includeBuild("../../../packages/gradle-plugin") 
}
plugins { 
    id("com.facebook.react.settings")  // ← 应用 ReactSettingsPlugin
}
extensions.configure(com.facebook.react.ReactSettingsExtension) { ex -> 
    ex.autolinkLibrariesFromCommand()  // ← 调用 autolinking 方法
}
完整调用流程图
┌─────────────────────────────────────────────────────────────────┐
│ 阶段 1: Gradle Settings 解析                                     │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│ settings.gradle 解析时:                                         │
│ ┌──────────────────────────────────────────────────────────┐   │
│ │ 1. 应用 ReactSettingsPlugin                              │   │
│ │    ↓                                                      │   │
│ │ 2. 注册 ReactSettingsExtension                           │   │
│ │    ↓                                                      │   │
│ │ 3. 调用 autolinkLibrariesFromCommand()                   │   │
│ └──────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────┘
                                  ↓
┌─────────────────────────────────────────────────────────────────┐
│ 阶段 2: 缓存检查和 CLI 执行                                      │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│ checkAndUpdateCache():                                          │
│ ┌──────────────────────────────────────────────────────────┐   │
│ │ 1. isCacheDirty() 检查缓存是否失效                        │   │
│ │    - 检查 autolinking.json 是否存在                       │   │
│ │    - 检查锁文件 SHA 是否变化                               │   │
│ │    - 检查配置模型是否有效                                  │   │
│ │                                                           │   │
│ │ 2. 如果缓存失效,执行 updateConfig.execute()              │   │
│ │    - 执行 npx @react-native-community/cli config         │   │
│ │    - 将输出写入 autolinking.json                          │   │
│ │    - 更新锁文件 SHA 缓存                                   │   │
│ └──────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────┘
                                  ↓
┌─────────────────────────────────────────────────────────────────┐
│ 阶段 3: 库发现和链接                                             │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│ linkLibraries(getLibrariesToAutolink()):                       │
│ ┌──────────────────────────────────────────────────────────┐   │
│ │ 1. JsonUtils.fromAutolinkingConfigJson() 解析 JSON       │   │
│ │    - 清理 CLI 输出中的调试信息                             │   │
│ │    - 反序列化为 ModelAutolinkingConfigJson                │   │
│ │                                                           │   │
│ │ 2. getLibrariesToAutolink() 提取可链接库                  │   │
│ │    - 过滤有 Android 配置的依赖                             │   │
│ │    - 跳过纯 C++ 依赖                                       │   │
│ │    - 映射项目名到源码目录                                  │   │
│ │                                                           │   │
│ │ 3. linkLibraries() 实际链接                               │   │
│ │    - settings.include(path) 包含子项目                    │   │
│ │    - settings.project(path).projectDir = dir 设置目录     │   │
│ └──────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────┘
ReactPlugin

与 ReactSettingsPlugin 不同,它是一个Project Plugin。该插件是在Gradle Configuration 阶段执行,负责任务注册和构建配置。

其核心职责是:

  • 插件初始化:创建 ReactExtension 配置扩展
  • 多项目协调:管理 App 和 Library 项目的不同配置
  • 任务注册:注册 Autolinking、Codegen 等关键任务
  • AGP 集成:与 Android Gradle Plugin 深度集成
  • 依赖配置:读取 ReactSettingsPlugin 的输出并进行后续处理

源码react-native/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt

class ReactPlugin : Plugin<Project> {
  override fun apply(project: Project) {
    // 第一步:JVM 版本检查
    checkJvmVersion(project)

    // 第二步:创建 ReactExtension 配置扩展
    // 这允许用户在 build.gradle 中使用 react { ... } 配置块
    val extension = project.extensions.create("react", ReactExtension::class.java, project)

    // 第三步:创建或获取根项目的私有扩展
    // 我们在 rootProject 上注册一个私有扩展,以便将项目范围的配置(如代码生成配置)从应用程序项目传播到库。
    val rootExtension = project.rootProject.extensions.findByType(PrivateReactExtension::class.java)
        ?: project.rootProject.extensions.create(
            "privateReact",
            PrivateReactExtension::class.java,
            project,
        )

    // 第四步:Hermes V1 配置
    if (project.rootProject.isHermesV1Enabled) {
      rootExtension.hermesV1Enabled.set(true)
    }

    // 第五步:应用项目特定配置
    project.pluginManager.withPlugin("com.android.application") {
      // 我们将根扩展与来自应用的值(用户填充或默认值)连接起来。
      rootExtension.root.set(extension.root)
      rootExtension.reactNativeDir.set(extension.reactNativeDir)
      rootExtension.codegenDir.set(extension.codegenDir)
      rootExtension.nodeExecutableAndArgs.set(extension.nodeExecutableAndArgs)

      // 延迟配置:在项目评估完成后执行
      project.afterEvaluate {
        // 在所有用户配置完成后执行
        val reactNativeDir = extension.reactNativeDir.get().asFile
        val propertiesFile = File(reactNativeDir, "ReactAndroid/gradle.properties")
        val hermesVersionPropertiesFile =
            File(reactNativeDir, "sdks/hermes-engine/version.properties")
        // 读取 React Native 版本信息
        val versionAndGroupStrings =
            readVersionAndGroupStrings(propertiesFile, hermesVersionPropertiesFile)
        val hermesV1Enabled = rootExtension.hermesV1Enabled.get()
        // 配置依赖和仓库
        configureDependencies(project, versionAndGroupStrings, hermesV1Enabled)
        configureRepositories(project)
      }

      // NDK 配置
      configureReactNativeNdk(project, extension)
      // 为应用配置 BuildConfig 字段
      configureBuildConfigFieldsForApp(project, extension)
      // 配置开发服务器位置
      configureDevServerLocation(project)
      // 配置向后兼容性 React 映射
      configureBackwardCompatibilityReactMap(project)
      // 配置 Java 工具链
      configureJavaToolChains(project)

      // AGP 变体配置
      project.extensions.getByType(ApplicationAndroidComponentsExtension::class.java).apply {
        onVariants(selector().all()) { variant ->
          // 为每个构建变体(debug、release 等)配置 React 任务
          project.configureReactTasks(variant = variant, config = extension)
        }
      }

      // 关键:配置 Autolinking
      configureAutolinking(project, extension)
      // 配置 Codegen(应用项目)
      configureCodegen(project, extension, rootExtension, isLibrary = false)
      // 配置 Android 资源
      configureResources(project, extension)
      // 为应用配置构建类型
      configureBuildTypesForApp(project)
  }

    // 库项目专用配置
    configureBuildConfigFieldsForLibraries(project)
    configureNamespaceForLibraries(project)
    project.pluginManager.withPlugin("com.android.library") {
      configureCodegen(project, extension, rootExtension, isLibrary = true)
    }
}

这里面关于configureCodegen方法调用,也是一个重要内容,但我们此文主要是分析三方TurboModule库的注册链接,暂时按下不表,在后面专项分析codegen机制时再来重点关注。

我们先来重点关注configureAutolinking方法,这是 React Native Autolinking 机制在 Gradle Configuration 阶段的核心实现。该方法负责注册所有 Autolinking 相关的 Gradle 任务,配置目录结构,并与 Android Gradle Plugin 进行深度集成。

/** 此函数为应用用户设置 Autolinking */
private fun configureAutolinking(
      project: Project,          // ← 当前 Gradle 项目实例
      extension: ReactExtension, // ← React Native 配置扩展
  ) {
    // 配置生成的 Java 源码目录(位于build目录下)
    // (存放生成的 Java 文件(PackageList.java、ReactNativeApplicationEntryPoint.java))
    val generatedAutolinkingJavaDir: Provider<Directory> =
        project.layout.buildDirectory.dir("generated/autolinking/src/main/java")
    // 配置生成的 JNI 源码目录(存放生成的 C++ 文件,为 React Native 新架构提供 C++ 绑定)
    val generatedAutolinkingJniDir: Provider<Directory> =
        project.layout.buildDirectory.dir("generated/autolinking/src/main/jni")

    // autolinking.json 文件在根构建文件夹中可用,因为它是由ReactSettingsPlugin.kt 生成的
    val rootGeneratedAutolinkingFile =
        project.rootProject.layout.buildDirectory.file("generated/autolinking/autolinking.json")

    // 我们添加一个名为 generateAutolinkingPackageList 的任务,
    // 以免与现有的名为 generatePackageList 的任务冲突。
    // 一旦我们解除 rn <-> cli依赖关系,这可以重命名。
    val generatePackageListTask =
        project.tasks.register(
            "generateAutolinkingPackageList",
            GeneratePackageListTask::class.java,
        ) { task ->
          task.autolinkInputFile.set(rootGeneratedAutolinkingFile)        // 设置输入文件:autolinking.json
          task.generatedOutputDirectory.set(generatedAutolinkingJavaDir) 
          // ↑ 设置输出目录:build/generated/autolinking/src/main/java
        }

    val generateEntryPointTask =
        project.tasks.register(
            "generateReactNativeEntryPoint",
            GenerateEntryPointTask::class.java,
        ) { task ->
          task.autolinkInputFile.set(rootGeneratedAutolinkingFile)
          task.generatedOutputDirectory.set(generatedAutolinkingJavaDir)
        }

    // 我们还需要为 C++ Autolinking 生成代码
    val generateAutolinkingNewArchitectureFilesTask =
        project.tasks.register(
            "generateAutolinkingNewArchitectureFiles",
            GenerateAutolinkingNewArchitecturesFileTask::class.java,
        ) { task ->
          task.autolinkInputFile.set(rootGeneratedAutolinkingFile)
          task.generatedOutputDirectory.set(generatedAutolinkingJniDir)
        }

    // 配置 C++ 任务依赖(执行顺序generateAutolinkingNewArchitectureFiles → preBuild)
    project.tasks
        .named("preBuild", Task::class.java)
        .dependsOn(generateAutolinkingNewArchitectureFilesTask)

    // 我们让 generateAutolinkingPackageList 和 generateEntryPoint 依赖于 preBuild 任务,
    // 这样它就会在其他所有任务之前执行。
    project.tasks
        .named("preBuild", Task::class.java)
        .dependsOn(generatePackageListTask, generateEntryPointTask)

    // 我们告诉 Android Gradle Plugin,在 /build/generated/autolinking/src/main/java 目录中
    // 也有需要编译的源码。
    project.extensions.getByType(ApplicationAndroidComponentsExtension::class.java).apply {
      onVariants(selector().all()) { variant ->  
        variant.sources.java?.addStaticSourceDirectory( // 源码目录添加
            generatedAutolinkingJavaDir.get().asFile.absolutePath
        )
      }
    }
  }

完整任务执行流程图

任务执行顺序:
┌─────────────────────────────────────────────────────────────────┐
│ 1. generateAutolinkingPackageList                               │
│    └── 生成 PackageList.java                                    │
├─────────────────────────────────────────────────────────────────┤
│ 2. generateReactNativeEntryPoint                                │
│    └── 生成 ReactNativeApplicationEntryPoint.java              │
├─────────────────────────────────────────────────────────────────┤
│ 3. generateAutolinkingNewArchitectureFiles                     │
│    └── 生成 C++ 新架构代码                                       │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│ 4. preBuild                                                     │
│    └── AGP 预构建任务                                            │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│ 5. 后续编译任务                                                  │
│    ├── compileDebugJavaWithJavac                               │
│    ├── compileDebugKotlin                                      │
│    └── 其他编译任务...                                           │
└─────────────────────────────────────────────────────────────────┘

流程已经非常清晰,但是有两个地方需要特别注意,一个是代码生成的目录不要搞错,这里是有两个build目录的:

YourReactNativeApp/                  ← 根项目
├── android/                         ← Android 应用模块
    └── app
    │        └──build/
    │               └── generated/autolinking/   ← ReactPlugin 生成的文件
    └── build/                             ← 根项目构建目录
        └── generated/
            └── autolinking/
                └── autolinking.json     ← ReactSettingsPlugin 生成的文件

第二个是AGP 源码目录集成之后的项目结构:

Android 项目源码结构:
app
└── src/
    └── main/
    │   ├── java/                                   ← 标准 Java 源码
    │   └── kotlin/                                 ← 标准 Kotlin 源码
    └── build/generated/autolinking/src/main/java/  ← 动态添加的源码目录
        └── com/facebook/react/
            ├── PackageList.java                    ← 生成的文件
            └── ReactNativeApplicationEntryPoint.java

这里我们有必要了解一下AGP 集成原理。上面代码中是有通过addStaticSourceDirectory添加源码目录的,首先为什么需要手动添加源码目录?

  • 动态生成:源码是在构建时动态生成的,不在标准源码目录中
  • AGP 感知:AGP 需要知道这些文件的存在才能编译它们
  • 变体支持:确保所有构建变体都包含生成的代码
  • 增量编译:AGP 可以跟踪这些文件的变化,支持增量编译
GeneratePackageListTask

接下来我们需要分析GeneratePackageListTask类的实现,这关系到三方TurboModule 模块的收集。

源码react-native/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GeneratePackageListTask.kt

abstract class GeneratePackageListTask : DefaultTask() {

  init {
    group = "react"
  }

  @get:InputFile 
  abstract val autolinkInputFile: RegularFileProperty      // ← 输入:autolinking.json

  @get:OutputDirectory 
  abstract val generatedOutputDirectory: DirectoryProperty // ← 输出:生成目录

  @TaskAction
  fun taskAction() {
    // 第一步:解析 autolinking.json
    val model = JsonUtils.fromAutolinkingConfigJson(autolinkInputFile.get().asFile)
        ?: error(
                """
                  RNGP - Autolinking: Could not parse autolinking config file:
                  ${autolinkInputFile.get().asFile.absolutePath}

                  The file is either missing or not containing valid JSON so the build won't succeed. 
                """
                    .trimIndent()
            )

    // 第二步:提取 Android 包名
    val packageName = model.project?.android?.packageName
        ?: error("RNGP - Autolinking: Could not find project.android.packageName")

    // 第三步:过滤 Android 包
    val androidPackages = filterAndroidPackages(model)

    // 第四步:生成 import 语句
    val packageImports = composePackageImports(packageName, androidPackages)

    // 第五步:生成实例化代码
    val packageClassInstance = composePackageInstance(packageName, androidPackages)

    // 第六步:组合最终文件内容
    val generatedFileContents = composeFileContent(packageImports, packageClassInstance)

    // 第七步:写入生成的文件
    val outputDir = generatedOutputDirectory.get().asFile
    outputDir.mkdirs()
    File(outputDir, GENERATED_FILENAME).apply {
        parentFile.mkdirs()
        writeText(generatedFileContents)
    }
  }
}

过滤 Android 包

  • 平台检查:只保留有 Android 平台配置的依赖
  • 类型过滤:排除纯 C++ 依赖(它们不需要 Java 包装)
  • 数据转换:将列表转换为 Map<依赖名, Android配置> 格式
internal fun filterAndroidPackages(
    model: ModelAutolinkingConfigJson?
): Map<String, ModelAutolinkingDependenciesPlatformAndroidJson> {
    val packages = model?.dependencies?.values ?: emptyList()
    return packages
        .filter { it.platforms?.android != null }                         // ← 必须有 Android 配置
        .filterNot { it.platforms?.android?.isPureCxxDependency == true } // ← 排除纯 C++ 依赖
        .associate { it.name to checkNotNull(it.platforms?.android) }     // ← 构建映射
}

来看一下生成 PackageList.java 的模板字符串,就会更加清晰:

val generatedFileContentsTemplate =
        """
        package com.facebook.react;

        import android.app.Application;
        import android.content.Context;
        import android.content.res.Resources;

        import com.facebook.react.ReactPackage;
        import com.facebook.react.shell.MainPackageConfig;
        import com.facebook.react.shell.MainReactPackage;
        import java.util.Arrays;
        import java.util.ArrayList;

        {{ packageImports }}

        @SuppressWarnings("deprecation")
        public class PackageList {
          private Application application;
          private ReactNativeHost reactNativeHost;
          private MainPackageConfig mConfig;

          public PackageList(ReactNativeHost reactNativeHost) {
            this(reactNativeHost, null);
          }

          public PackageList(Application application) {
            this(application, null);
          }

          public PackageList(ReactNativeHost reactNativeHost, MainPackageConfig config) {
            this.reactNativeHost = reactNativeHost;
            mConfig = config;
          }

          public PackageList(Application application, MainPackageConfig config) {
            this.reactNativeHost = null;
            this.application = application;
            mConfig = config;
          }

          private ReactNativeHost getReactNativeHost() {
            return this.reactNativeHost;
          }

          private Resources getResources() {
            return this.getApplication().getResources();
          }

          private Application getApplication() {
            if (this.reactNativeHost == null) return this.application;
            return this.reactNativeHost.getApplication();
          }

          private Context getApplicationContext() {
            return this.getApplication().getApplicationContext();
          }

          public ArrayList<ReactPackage> getPackages() {
            return new ArrayList<>(Arrays.<ReactPackage>asList(
              new MainReactPackage(mConfig){{ packageClassInstances }}
            ));
          }
        }
        """
            .trimIndent()

注意到,这里是通过new MainReactPackage(mConfig){{ packageClassInstances }} 动态插入了收集到的三方TurboModule 。

GenerateAutolinkingNewArchitecturesFileTask

接下来再研究一下GenerateAutolinkingNewArchitecturesFileTask类,这关系到我们前面说的纯C++ TurboModule的自动链接注册。

源码react-native/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GenerateAutolinkingNewArchitecturesFileTask.kt

abstract class GenerateAutolinkingNewArchitecturesFileTask : DefaultTask() {
  init {
    group = "react"
  }
  @get:InputFile abstract val autolinkInputFile: RegularFileProperty
  //  输出:build/generated/autolinking/src/main/jni/
  @get:OutputDirectory abstract val generatedOutputDirectory: DirectoryProperty

  @TaskAction
  fun taskAction() {
    // 1. 解析 autolinking.json
    val model = JsonUtils.fromAutolinkingConfigJson(autolinkInputFile.get().asFile)
    // 2. 过滤 Android 平台的包
    val packages = filterAndroidPackages(model)
    // 3. 生成 CMake 文件内容
    val cmakeFileContent = generateCmakeFileContent(packages)
    // 4. 生成 C++ 文件内容
    val cppFileContent = generateCppFileContent(packages)
    // 5. 写入文件
    val outputDir = generatedOutputDirectory.get().asFile
    outputDir.mkdirs()
    File(outputDir, CMAKE_FILENAME).apply { writeText(cmakeFileContent) }
    File(outputDir, CPP_FILENAME).apply { writeText(cppFileContent) }
    File(outputDir, H_FILENAME).apply { writeText(hTemplate) }
  }

  internal fun filterAndroidPackages(
      model: ModelAutolinkingConfigJson?
  ): List<ModelAutolinkingDependenciesPlatformAndroidJson> =
      // 从dependencies中提取所有Android平台的配置,如果为空则返回空列表
      model?.dependencies?.values?.mapNotNull { it.platforms?.android } ?: emptyList()

  internal fun generateCmakeFileContent(
      packages: List<ModelAutolinkingDependenciesPlatformAndroidJson>
  ): String {
    val libraryIncludes =
        packages.joinToString("\n") { dep ->
          var addDirectoryString = ""
          val libraryName = dep.libraryName  // 获取库名称
          // 获取CMakeLists.txt文件路径
          // 用于Codegen生成的TurboModule/Fabric组件,生成的库名:react_codegen_${libraryName}
          // 通过 autolinking_ModuleProvider() 提供 Java TurboModule    
          val cmakeListsPath = dep.cmakeListsPath

          // 用于纯C++ TurboModule,生成的库名由第三方库的CMakeLists.txt自定义
          // 通过 autolinking_cxxModuleProvider() 提供 C++ TurboModule
          val cxxModuleCMakeListsPath = dep.cxxModuleCMakeListsPath
          if (libraryName != null && cmakeListsPath != null) {
            // 如果用户提供了自定义的 cmakeListsPath,则使用它
            val nativeFolderPath = sanitizeCmakeListsPath(cmakeListsPath)
            addDirectoryString +=
                "add_subdirectory(\"$nativeFolderPath\" ${libraryName}_autolinked_build)"
          }
          if (cxxModuleCMakeListsPath != null) {
            // 如果用户提供了自定义的cxxModuleCMakeListsPath,则使用它
            val nativeFolderPath = sanitizeCmakeListsPath(cxxModuleCMakeListsPath)
            addDirectoryString +=
                "\nadd_subdirectory(\"$nativeFolderPath\" ${libraryName}_cxxmodule_autolinked_build)"
          }
          addDirectoryString
        }

    // 生成库模块列表:收集所有需要自动链接的库名称
    val libraryModules =
        packages.joinToString("\n  ") { dep ->
          var autolinkedLibraries = ""
          // 如果存在库名称,添加代码生成库前缀
          if (dep.libraryName != null) {
            autolinkedLibraries += "$CODEGEN_LIB_PREFIX${dep.libraryName}"
          }
          if (dep.cxxModuleCMakeListsModuleName != null) {
            autolinkedLibraries += "\n${dep.cxxModuleCMakeListsModuleName}"
          }
          autolinkedLibraries
        }

    return CMAKE_TEMPLATE.replace("{{ libraryIncludes }}", libraryIncludes)
        .replace("{{ libraryModules }}", libraryModules)
  }

  internal fun generateCppFileContent(
      packages: List<ModelAutolinkingDependenciesPlatformAndroidJson>
  ): String {
    // 过滤出有库名称的包(只有这些包才需要生成C++代码)
    val packagesWithLibraryNames = packages.filter { android -> android.libraryName != null }

    val cppIncludes =
        packagesWithLibraryNames.joinToString("\n") { dep ->
          var include = "#include <${dep.libraryName}.h>"
          if (dep.componentDescriptors.isNotEmpty()) {
            include +=
                "\n#include <${COMPONENT_INCLUDE_PATH}/${dep.libraryName}/${COMPONENT_DESCRIPTOR_FILENAME}>"
          }
          if (dep.cxxModuleHeaderName != null) {
            include += "\n#include <${dep.cxxModuleHeaderName}.h>"
          }
          include
        }

    // 生成Java TurboModule提供者的C++代码
    val cppTurboModuleJavaProviders =
        packagesWithLibraryNames.joinToString("\n") { dep ->
          val libraryName = dep.libraryName
          // language=cpp
          """  
      auto module_$libraryName = ${libraryName}_ModuleProvider(moduleName, params);
      if (module_$libraryName != nullptr) {
      return module_$libraryName;
      }
      """
              .trimIndent()
        }

    // 生成C++ TurboModule提供者的代码
    val cppTurboModuleCxxProviders =
        packagesWithLibraryNames
            .filter { it.cxxModuleHeaderName != null }
            .joinToString("\n") { dep ->
              val cxxModuleHeaderName = dep.cxxModuleHeaderName
              // language=cpp
              """
      if (moduleName == $cxxModuleHeaderName::kModuleName) {
      return std::make_shared<$cxxModuleHeaderName>(jsInvoker);
      }
      """
                  .trimIndent()
            }

    val cppComponentDescriptors =
        packagesWithLibraryNames
            .filter { it.componentDescriptors.isNotEmpty() }
            .joinToString("\n") {
              it.componentDescriptors.joinToString("\n") {
                "providerRegistry->add(concreteComponentDescriptorProvider<$it>());"
              }
            }

    return CPP_TEMPLATE.replace("{{ autolinkingCppIncludes }}", cppIncludes)
        .replace("{{ autolinkingCppTurboModuleJavaProviders }}", cppTurboModuleJavaProviders)
        .replace("{{ autolinkingCppTurboModuleCxxProviders }}", cppTurboModuleCxxProviders)
        .replace("{{ autolinkingCppComponentDescriptors }}", cppComponentDescriptors)
  }

  companion object {
    const val CMAKE_FILENAME = "Android-autolinking.cmake"

    const val H_FILENAME = "autolinking.h"
    const val CPP_FILENAME = "autolinking.cpp"
    // React代码生成库的前缀
    const val CODEGEN_LIB_PREFIX = "react_codegen_"
    // 组件描述符头文件名
    const val COMPONENT_DESCRIPTOR_FILENAME = "ComponentDescriptors.h"
    // 组件的包含路径前缀
    const val COMPONENT_INCLUDE_PATH = "react/renderer/components"

   /**
     * 清理CMakeLists.txt路径,移除文件名部分并转义空格字符
     */
    internal fun sanitizeCmakeListsPath(cmakeListsPath: String): String =
        cmakeListsPath.replace("CMakeLists.txt", "").replace(" ", "\\ ")

    // language=cmake
    val CMAKE_TEMPLATE =
        """
        # This code was generated by [React Native](https://www.npmjs.com/package/@react-native/gradle-plugin)
        cmake_minimum_required(VERSION 3.13)
        set(CMAKE_VERBOSE_MAKEFILE on)

        # We set REACTNATIVE_MERGED_SO so libraries/apps can selectively decide to depend on either libreactnative.so
        # or link against a old prefab target (this is needed for React Native 0.76 on).
        set(REACTNATIVE_MERGED_SO true)

        {{ libraryIncludes }}

        set(AUTOLINKED_LIBRARIES
          {{ libraryModules }}
        )
        """
            .trimIndent()

    // language=cpp
    val CPP_TEMPLATE =
        """
        /**
         * This code was generated by [React Native](https://www.npmjs.com/package/@react-native/gradle-plugin).
         *
         * Do not edit this file as changes may cause incorrect behavior and will be lost
         * once the code is regenerated.
         *
         */

        #include "autolinking.h"
        {{ autolinkingCppIncludes }}

        namespace facebook {
        namespace react {

        std::shared_ptr<TurboModule> autolinking_ModuleProvider(const std::string moduleName, const JavaTurboModule::InitParams &params) {
        {{ autolinkingCppTurboModuleJavaProviders }}
          return nullptr;
        }

        std::shared_ptr<TurboModule> autolinking_cxxModuleProvider(const std::string moduleName, const std::shared_ptr<CallInvoker>& jsInvoker) {
        {{ autolinkingCppTurboModuleCxxProviders }}
          return nullptr;
        }

        void autolinking_registerProviders(std::shared_ptr<ComponentDescriptorProviderRegistry const> providerRegistry) {
        {{ autolinkingCppComponentDescriptors }}
          return;
        }

        } // namespace react
        } // namespace facebook
        """
            .trimIndent()

    // language=cpp
    val hTemplate =
        """
        /**
         * This code was generated by [React Native](https://www.npmjs.com/package/@react-native/gradle-plugin).
         *
         * Do not edit this file as changes may cause incorrect behavior and will be lost
         * once the code is regenerated.
         *
         */

        #pragma once

        #include <ReactCommon/CallInvoker.h>
        #include <ReactCommon/JavaTurboModule.h>
        #include <ReactCommon/TurboModule.h>
        #include <jsi/jsi.h>
        #include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>

        namespace facebook {
        namespace react {

        std::shared_ptr<TurboModule> autolinking_ModuleProvider(const std::string moduleName, const JavaTurboModule::InitParams &params);
        std::shared_ptr<TurboModule> autolinking_cxxModuleProvider(const std::string moduleName, const std::shared_ptr<CallInvoker>& jsInvoker);
        void autolinking_registerProviders(std::shared_ptr<ComponentDescriptorProviderRegistry const> providerRegistry);

        } // namespace react
        } // namespace facebook
        """
            .trimIndent()
  }
}

此处需要特别注意的是,有两个cmake路径:cmakeListsPathcxxModuleCMakeListsPath

  • cmakeListsPath:编译由Codegen生成的TurboModule/Fabric组件,库名称是react_codegen_${libraryName},使用autolinking_ModuleProvider()函数注册。简单说,就是从JS规范自动生成的相关代码,主要就基于原生的TurboModule。
  • cxxModuleCMakeListsPath:主要就是我们手写的纯C++TurboModule,库名称由我们自定义,使用autolinking_cxxModuleProvider()注册。也就是说,如果我们打算开发一个纯C++TurboModule,那么此路径就是必须要提供的

这里有一个疑问,前面我们分析了基于原生的TurboModule(即Java TurboModule)是通过自动生成PackageList类来实现注册的,那么这里的autolinking_ModuleProvider又是什么呢?是否出现了重复注册?

其实在上面Java TurboModule 分析一节,我们已经触及到了这个问题。回顾一下,那里提到了两层查找,首先是反射kotlin类的getTurboJavaModule方法,获取到了通过PackageList注册的TurboModule 的Kotlin类对象,然后将此对象封装成一个参数继续调用C++层的getTurboModule返回一个C++层的TurboModule对象,最终是将这个C++ TurboModule实例对象添加到缓存中,用于下次直接使用。也就是说,一个RN的第三方TurboModule库,其方法调用是使用这个C++ TurboModule实例对象,而不是Kotlin实现的TurboModule对象。

要想弄明白这个问题,可以创建一个hello项目,在项目中任意添加一个三方TurboModule。譬如我这里添加:"@dr.pogodin/react-native-fs",然后安装依赖,并构建项目,就会自动触发codegen。

现在我们来查看生成的autolinking_ModuleProvider,源码hello/android/app/build/generated/autolinking/src/main/jni/autolinking.cpp

std::shared_ptr<TurboModule> autolinking_ModuleProvider(const std::string moduleName, const JavaTurboModule::InitParams &params) {
  auto module_RNReactNativeFsSpec = RNReactNativeFsSpec_ModuleProvider(moduleName, params);
  if (module_RNReactNativeFsSpec != nullptr) {
      return module_RNReactNativeFsSpec;
  }
  return nullptr;
}

源码node_modules/@dr.pogodin/react-native-fs/android/generated/jni/RNReactNativeFsSpec-generated.cpp

std::shared_ptr<TurboModule> RNReactNativeFsSpec_ModuleProvider(const std::string &moduleName, const JavaTurboModule::InitParams &params) {
  if (moduleName == "ReactNativeFs") {
    return std::make_shared<NativeReactNativeFsSpecJSI>(params);
  }
  return nullptr;
}

NativeReactNativeFsSpecJSI::NativeReactNativeFsSpecJSI(const JavaTurboModule::InitParams &params)
  : JavaTurboModule(params) {
  methodMap_["getConstants"] = MethodMetadata {0, __hostFunction_NativeReactNativeFsSpecJSI_getConstants};
  methodMap_["addListener"] = MethodMetadata {1, __hostFunction_NativeReactNativeFsSpecJSI_addListener};
  methodMap_["removeListeners"] = MethodMetadata {1, __hostFunction_NativeReactNativeFsSpecJSI_removeListeners};
  methodMap_["appendFile"] = MethodMetadata {2, __hostFunction_NativeReactNativeFsSpecJSI_appendFile};
  methodMap_["copyFile"] = MethodMetadata {3, __hostFunction_NativeReactNativeFsSpecJSI_copyFile};
  // 省略......
}

static facebook::jsi::Value __hostFunction_NativeReactNativeFsSpecJSI_getConstants(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
  static jmethodID cachedMethodId = nullptr;
  return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, ObjectKind, "getConstants", "()Ljava/util/Map;", args, count, cachedMethodId);
}

最终返回的是一个自动生成的NativeReactNativeFsSpecJSI对象指针。我们看一下声明:

/**
 * JNI C++ class for module 'NativeReactNativeFs'
 */
class JSI_EXPORT NativeReactNativeFsSpecJSI : public JavaTurboModule {
public:
  NativeReactNativeFsSpecJSI(const JavaTurboModule::InitParams &params);
};

可见,NativeReactNativeFsSpecJSI就是一个派生自JavaTurboModule的子类。在其构造方法中,为methodMap_注册了很多方法。这些方法也就是提供给上层JS调用的。查看任意一个方法,发现它还是反射的Kotlin的TurboModule实现类的方法。也就是说,绕了一大圈,最终调用的仍然是Kotlin编写的TurboModule实现。生成的PackageList仅用于首次注册模块,后续的方法调用基本都是基于NativeReactNativeFsSpecJSI的反射。当然,这里的反射也进行了一些优化,只会反射一次。

推测之所以要这样做,大概就是有两点,一是为了兼容旧架构基于PackageList注册模块的机制,第二是为了支持更自动化的Codegen。如果直接反射Kotlin的TurboModule实现类完成调用,就没办法自动规范化接口。简单说,当我们用TS定义好接口API后,Codegen可以根据接口的函数签名,自动对参数、返回值类型进行JSI类型映射,无需模块开发者操心。NativeReactNativeFsSpecJSI类就是自动生成的方法类型映射,这是基于编译时生成的,相比运行时手动处理更安全。

再看一下该三方库在node_modules中的文件结构:

node_modules/                  
├── @dr.pogodin/react-native-fs                         ← 三方TurboModule
    └── android
             └──generated/
                    └── jni/
                                    └── CMakeLists.txt
                                    |── RNReactNativeFsSpec.h
                                    |── RNReactNativeFsSpec-generated.cpp

显然的,这里的CMakeLists.txt就是上面cmakeListsPath路径指向的文件,用于编译当前目录下自动生成的C++代码RNReactNativeFsSpec-generated.cpp

最后总结一下,一个React Native项目中的cmake关系:

hello/
├── android/
│   ├── settings.gradle                              ⓪ Gradle 配置入口
│   ├── build.gradle                                 ⓪ 根 build.gradle
│   │
│   └── app/
│       ├── build.gradle                             ⓪ App build.gradle (应用 react 插件)
│       │
│       ├── src/main/
│       │   └── res/...
│       │
│       └── build/
│           ├── generated/
│           │   └── autolinking/
│           │       └── src/main/
│           │           ├── java/com/facebook/react/
│           │           │   └── PackageList.java   ⓹ 生成的 Java 包列表
│           │           │
│           │           └── jni/
│           │               ├── Android-autolinking.cmake  ⓷ 生成的 CMake 配置
│           │               ├── autolinking.h              ⓹ 生成的 C++ 头文件
│           │               └── autolinking.cpp            ⓸ 生成的 C++ 实现
│
└── node_modules/
    ├── react-native/
    │   └── ReactAndroid/
    │       └── cmake-utils/
    │           ├── default-app-setup/
    │           │   ├── CMakeLists.txt             ⓵ 入口 CMakeLists.txt
    │           │   └── OnLoad.cpp                 ⓺ JNI 入口点
    │           │
    │           ├── ReactNative-application.cmake  ⓶ 核心构建逻辑
    │           └── folly-flags.cmake              ⓪ Folly 编译选项
    │
    └── react-native-third-party/
        └── android/
            ├── build.gradle                       ⓪ 第三方库 Gradle
            └── build/generated/source/codegen/jni/
                └── CMakeLists.txt                 ⓺ 第三方库 CMake

CMake包含关系:

default-app-setup/CMakeLists.txt
│
├─ include(${REACT_ANDROID_DIR}/cmake-utils/ReactNative-application.cmake)
│  │
│  ReactNative-application.cmake
│  │
│  ├─ include(${CMAKE_CURRENT_LIST_DIR}/folly-flags.cmake)
│  │  └─ 设置 Folly 编译选项
│  │
│  ├─ include(${PROJECT_BUILD_DIR}/generated/autolinking/src/main/jni/Android-autolinking.cmake)
│  │  │
│  │  Android-autolinking.cmake
│  │  │
│  │  └─ add_subdirectory(
│  │        "/path/to/node_modules/react-native-third-party/android/generated/jni/"
│  │        RNReactNativeFsSpec_autolinked_build
│  │     )
│  │     │
│  │     react-native-third-party/CMakeLists.txt
│  │     │
│  │     └─ add_library(react_codegen_RNReactNativeFsSpec OBJECT ${react_codegen_SRCS})
│  │
│  └─ add_subdirectory(${BUILD_DIR}/generated/source/codegen/jni/)
│     └─ App 级别的 Codegen (如果存在)
│
└─ 创建 libappmodules.so

在React Native 的原生项目中,并没有做CMake相关的配置,那么到底是哪里触发的CMake构建的呢?

CMake 链路图:

┌──────────────────────────────────────────────────────────────────────┐
│ ⓪ android/app/build.gradle                                           │
├──────────────────────────────────────────────────────────────────────┤
│ apply plugin: "com.facebook.react"                                   │
│                                                                       │
│ react {                                                              │
│     autolinkLibrariesWithApp()  ← 启用 autolinking                   │
│ }                                                                    │
│                                                                       │
│ android {                                                            │
│     ndkVersion rootProject.ext.ndkVersion                           │
│     // 注意:并没有 externalNativeBuild 配置!                       │
│ }                                                                    │
└──────────────────────────────────────────────────────────────────────┘
                              ↓
┌──────────────────────────────────────────────────────────────────────┐
│ ReactPlugin.kt (apply 插件时执行)                                     │
├──────────────────────────────────────────────────────────────────────┤
│ import com.facebook.react.utils.NdkConfiguratorUtils.configureReactNativeNdk|
|                                                                      |
|   override fun apply(project: Project) {                             │
│     configureReactNativeNdk(project, extension)  ← 配置 NDK          │
│     configureAutolinking(project, extension)     ← 配置 autolinking  │
│   }                                                                  │
└──────────────────────────────────────────────────────────────────────┘
                              ↓
┌──────────────────────────────────────────────────────────────────────┐
│ NdkConfiguratorUtils.kt (自动配置 CMake)                             │
├──────────────────────────────────────────────────────────────────────┤
│ val ext = project.extensions.getByType(BaseExtension::class.java)   │
│ ext.buildFeatures.prefab = true                                     │
│                                                                       │
│ // 关键代码:如果用户没有提供 CMakeLists.txt,使用默认的           │if (ext.externalNativeBuild.cmake.path == null) {                   │
│     ext.externalNativeBuild.cmake.path = File(                      │
│         extension.reactNativeDir.get().asFile,                                        │
│         "ReactAndroid/cmake-utils/default-app-setup/CMakeLists.txt" │
│     )                                                                │
│ }                                                                    │
│                                                                       │
│ // 添加 CMake 参数                                                    │val cmakeArgs = ext.defaultConfig.externalNativeBuild.cmake.arguments│
│ cmakeArgs.add("-DPROJECT_BUILD_DIR=${project.layout.buildDirectory.get().asFile}")      │
│ cmakeArgs.add("-DREACT_ANDROID_DIR=${extension.reactNativeDir.file("ReactAndroid").get().asFile}") │
│ cmakeArgs.add("-DANDROID_STL=c++_shared")                           │
│                                                                       │
│ 结果:                                                                │
│ externalNativeBuild.cmake.path =                                    │
│   "/path/to/node_modules/react-native/ReactAndroid/cmake-utils/     │
│    default-app-setup/CMakeLists.txt"                                │
└──────────────────────────────────────────────────────────────────────┘

可见,是com.facebook.react插件脚本中动态注入了构建CMake的配置,从而触发了CMake构建。

流程总结
  • Settings 阶段ReactSettingsPluginsettings.gradle 解析时自动执行
  • 配置阶段ReactPlugin 在项目配置时注册 GeneratePackageListTask
  • 构建阶段preBuild 任务自动依赖 generateAutolinkingPackageList 任务
  • 编译阶段:生成的 PackageList.java 被自动编译到应用中

完整的任务流程图

┌─────────────────────────────────────────────────────────────────┐
│ Gradle 构建                                          │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│ Settings 阶段:                                                  │
│ ┌──────────────────────────────────────────────────────────┐   │
│ │ ReactSettingsPlugin.apply()                              │   │
│ │   ↓                                                      │   │
│ │ ReactSettingsExtension.autolinkLibrariesFromCommand()    │   │
│ │   ↓                                                      │   │
│ │ 生成 autolinking.json                                    │   │
│ └──────────────────────────────────────────────────────────┘   │
│                                  ↓                              │
│ Configuration 阶段:                                             │
│ ┌──────────────────────────────────────────────────────────┐   │
│ │ ReactPlugin.apply()                                      │   │
│ │   ↓                                                      │   │
│ │ configureAutolinking()                                   │   │
│ │   ↓                                                      │   │
│ │ 注册 Autolinking 任务:                                   │   │
│ │ - generateAutolinkingPackageList                         │   │
│ │ - generateReactNativeEntryPoint                          │   │
│ │ - generateAutolinkingNewArchitectureFiles                │   │
│ └──────────────────────────────────────────────────────────┘   │
│                                  ↓                              │
│ Execution 阶段:                                                 │
│ ┌──────────────────────────────────────────────────────────┐   │
│ │ preBuild                                                 │   │
│ │   ├── generateAutolinkingPackageList                     │   │
│ │   │   └── 生成 PackageList.java                          │   │
│ │   ├── generateReactNativeEntryPoint                      │   │
│ │   │   └── 生成 ReactNativeApplicationEntryPoint.java     │   │
│ │   └── generateAutolinkingNewArchitectureFiles            │   │
│ │       └── 生成 C++ 新架构代码                             │   │
│ │                                                           │   │
│ │ compileDebugJavaWithJavac                                │   │
│ │   └── 编译生成的 Java 代码                                │   │
│ └──────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────┘

抗干扰LED数显屏驱动VK1624 数码管显示芯片 3线串行接口

VK1624是一种数码管或点阵LED驱动控制专用芯片,内部集成有3线串行接口、数据锁 存器、LED 驱动等电路。SEG脚接LED阳极,GRID脚接LED阴极,可支持 14SEGx4GRID、13SEGx5GRID、12SEGx6GRID、11SEGx7GRID的点阵LED显示面板。适用 于要求可靠、稳定和抗干扰能力强的产品。采用 SOP24/DIP24 的封装形式。LJQ7478

产品品牌:永嘉微电/VINKA
产品型号:VK1624

封装形式:SOP24

  VK1624_V1.3-CN_04.png

特点

• 工作电压 3.0-5.5V

• 内置 RC振荡器

• 11个SEG脚,4个GRID脚,3个可配置SEG/GRID复用脚

• SEG脚只能接LED阳极,GRID脚只能接LED阴极

• 3线串行接口

• 8级整体亮度可调

• 内置显示RAM为14x8位

• 内置上电复位电路

• 抗干扰能力强

• 封装

SOP24(300mil)(15.40mm x 7.50mm PP=1.27mm).......

VK1624_V1.3-CN_05.png

VK1624_V1.3-CN_06.png内存映射的LED控制器及驱动器****

VK16D32   3.0~5.5V  驱动点阵:96    共阴驱动:8段12位   共阳驱动:---   通讯接口:SCL/SDA   静态****

电流/待机电流:<1mA<10μA   按键:---   封装:SSOP24  恒流驱动

VK16D33   3.0~5.5V  驱动点阵:128   共阴驱动:8段16位   共阳驱动:---   通讯接口:SCL/SDA   静态

电流/待机电流:<1mA<10μA   按键:---   封装:SOP28   恒流驱动
———————————————————————————————————————————————————

VK16K33A   3.0~5.5V   驱动点阵:128  共阴驱动:16段8位; 共阳驱动:8段16位    通讯接口:SCL/SDA   

静态电流/待机电流:typ.1mA/1μA   按键:13*3     封装:SOP28    驱动电流大,适合高亮显示场合

VK16K33AA  3.0~5.5V   驱动点阵:128  共阴驱动:16段8位; 共阳驱动:8段16位    通讯接口:SCL/SDA   

静态电流/待机电流:typ.1mA/1μA   按键:13*3     封装:SSOP28   驱动电流大,适合高亮显示场合

VK16K33B  3.0~5.5V   驱动点阵:96    共阴驱动:12段8位; 共阳驱动:8段12位    通讯接口:SCL/SDA   

静态电流/待机电流:typ.1mA/1μA   按键:10*3     封装:SOP24   驱动电流大,适合高亮显示场合

VK16K33BA  3.0~5.5V   驱动点阵:96    共阴驱动:12段8位; 共阳驱动:8段12位    通讯接口:SCL/SDA   

静态电流/待机电流:typ.1mA/1μA   按键:10*3     封装:SSOP24  驱动电流大,适合高亮显示场合

VK16K33C   3.0~5.5V   驱动点阵:64    共阴驱动:8段8位;  共阳驱动:8段8位     通讯接口:SCL/SDA   

静态电流/待机电流:typ.1mA/1μA   按键:8*3      封装:SOP20   驱动电流大,适合高亮显示场合

——————————————————————————————————————————————————

VK1640    3.0~5.5V   驱动点阵:128   共阴驱动:8段16位   共阳驱动:16段8位      通讯接口:CLK/DIN

静态电流/待机电流:<0.1mA/--       按键:---   封装:SOP28   

VK1640A   3.0~5.5V   驱动点阵:128   共阴驱动:8段16位   共阳驱动:16段8位      通讯接口:CLK/DIN

静态电流/待机电流:<0.1mA/--       按键:---   封装:SSOP28

VK1640B   3.0~5.5V   驱动点阵:96    共阴驱动:8段12位   共阳驱动:12段8位      通讯接口:CLK/DIN

静态电流/待机电流:<0.1mA/--       按键:---   封装:SSOP24

VK1650    3.0~5.5V   驱动点阵:32    共阴驱动:8段4位   共阳驱动:4段8位        通讯接口:CLK/DAT

静态电流/待机电流:typ.0.3mA/50μA   按键:7*4   封装:SOP16/DIP16  

VK1Q60    3.0~5.5V   驱动点阵:32    共阴驱动:8段4位   共阳驱动:4段8位        通讯接口:CLK/DAT

静态电流/待机电流:<0.1mA/---     按键:7*4   封装:QFN16

VK1651    3.0~5.5V   驱动点阵:28    共阴驱动:4段7位   共阳驱动:7段4位        通讯接口:CLK/DIO

静态电流/待机电流:<5mA/--         按键:7*1   封装:SOP16/DIP16
——————————————————————————————————————————————————

VK1637    3.0~5.5V   驱动点阵:48    共阴驱动:6段8位   共阳驱动:8段6位        通讯接口:CLK/DIO

静态电流/待机电流:--/--            按键:8*2   封装:SOP20/DIP20               

VK1616    3.0~5.5V   驱动点阵:28    共阴驱动:7段4位   共阳驱动:4段7位        通讯接口:CLK/STB/DIO

静态电流/待机电流:<1mA/--         按键:---   封装:SOP16/DIP16       抗干扰能力强

VK1618    3.0~5.5V   驱动点阵:35/36/35/32    共阴驱动:5段7位;6段6位;7段5位;8段4位   

共阳驱动:7段5位;6段6位;5段7位;4段8位         通讯接口:CLK/STB/DIO

静态电流/待机电流:<1mA/--         按键:5*1   封装:SOP18/DIP18       抗干扰能力强

VK1620B   3.0~5.5V   驱动点阵:48/45/40      共阴驱动:8段6位;9段5位;10段4位   

共阳驱动:6段8位;5段9位;4段10位                  通讯接口:CLK/STB/DIO

静态电流/待机电流:<1mA/--         按键:---   封装:SOP20             抗干扰能力强

VK1624    3.0~5.5V   驱动点阵:77/72/65/56   共阴驱动:11段7位;12段6位;13段5位;14段4位   

共阳驱动:7段11位;6段12位;5段13位;4段14        通讯接口:CLK/STB/DIO

静态电流/待机电流:<1mA/--         按键:---   封装:SOP24/DIP24       抗干扰能力强

VK1S68C   3.0~5.5V   驱动点阵:70/66/60/52   共阴驱动:10段7位;11段6位;12段5位;13段4位  

共阳驱动:7段10位;6段11位;5段12位;4段13位     通讯接口:CLK/STB/DIO

静态电流/待机电流:<1mA/--         按键:10*2  封装:SSOP24            抗干扰能力强

VK1Q68D   3.0~5.5V   驱动点阵:70/66/60/52   共阴驱动:10段7位;11段6位;12段5位;13段4位  

共阳驱动:7段10位;6段11位;5段12位;4段13位     通讯接口:CLK/STB/DIO

静态电流/待机电流:<1mA/--         按键:10*2  封装:QFN24             抗干扰能力强

VK1668    3.0~5.5V   驱动点阵:70/66/60/52   共阴驱动:10段7位;11段6位;12段5位;13段4位  

共阳驱动:7段10位;6段11位;5段12位;4段13位     通讯接口:CLK/STB/DIO

静态电流/待机电流:<1mA/--         按键:10*2  封装:SOP24/SSOP24      抗干扰能力强

VK1628    3.0~5.5V   驱动点阵:70/66/60/52   共阴驱动:10段7位;11段6位;12段5位;13段4位  

共阳驱动:7段10位;6段11位;5段12位;4段13位     通讯接口:CLK/STB/DIO

静态电流/待机电流:<1mA/--         按键:10*2  封装:SOP28             抗干扰能力强

VK1S38A   3.0~5.5V   驱动点阵:64     共阴驱动:8段8位    共阳驱动:8段8位   通讯接口:CLK/STB/DIO

静态电流/待机电流:<5mA/--         按键:8*3   封装:SSOP24            抗干扰能力强

VK1638    3.0~5.5V   驱动点阵:80     共阴驱动:10段8位    共阳驱动:8段10位   通讯接口:CLK/STB/DIO

静态电流/待机电流:<5mA/--         按键:8*3   封装:SOP28             抗干扰能力强

VK1629    3.0~5.5V  驱动点阵:128  共阴驱动:16段8位   共阳驱动:8段16位   通讯接口:CLK/STB/DIN/DOUT

静态电流/待机电流:<5mA/--  按键:8*4   封装:LQFP44(QFP44正方形);   抗干扰能力强

VK1629A   3.0~5.5V   驱动点阵:128     共阴驱动:16段8位    共阳驱动:8段16位   通讯接口:CLK/STB/DIO

静态电流/待机电流:<5mA/--         按键:---   封装:SOP32             抗干扰能力强

VK1629B   3.0~5.5V   驱动点阵:112     共阴驱动:14段8位    共阳驱动:8段14位   通讯接口:CLK/STB/DIO

静态电流/待机电流:<5mA/--         按键:8*2   封装:SOP32             抗干扰能力强

VK1629C   3.0~5.5V   驱动点阵:120     共阴驱动:15段8位    共阳驱动:8段15位   通讯接口:CLK/STB/DIO

静态电流/待机电流:<5mA/--         按键:8*1   封装:SOP32             抗干扰能力强

VK1629D   3.0~5.5V   驱动点阵:96      共阴驱动:12段8位    共阳驱动:8段12位   通讯接口:CLK/STB/DIO

静态电流/待机电流:<5mA/--         按键:8*4   封装:SOP32             抗干扰能力强

VK6932    3.0~5.5V   驱动点阵:128     共阴驱动:8段16位    共阳驱动:16段8位   通讯接口:CLK/STB/DIN

静态电流/待机电流:<0.1mA/--       按键:---   封装:SOP32             抗干扰能力强

(永嘉微电/VINKA原厂-FAE技术支持,主营LCD驱动IC; LED驱动IC; 触摸IC; LDO稳压IC; 水位检测IC)

LED驱动、LED屏驱动、数显驱动IC、LED芯片、LED驱动器、数码管显示驱动、LED显示驱动、LED数显驱动原厂、LED数显驱动芯片、LED驱动IC、点阵LED显示驱动、LED屏驱动IC、数显驱动芯片、数码管芯片、数码管驱动、数显屏驱动、数显IC、数显芯片、数显驱动、LED数显IC、数显驱动原厂、LED屏驱动芯片、LED数显驱动IC、LED数显驱动IC、LED驱动电路、数显LED屏驱动、LED数显屏驱动、LED显示屏驱动、LED数码管驱动、数显LED驱动、LED数显驱动、数码管显示IC、数码管显示芯片、数码管驱动芯片、LED显示驱动芯片、显示数码管驱动、LED控制电路、数显LED驱动芯片、数显LED驱动IC、LED驱动芯片、数码管显示屏驱动、数码管驱动原厂、LED驱动厂家、LED驱动原厂、LED数码驱动、LED数码屏驱动、LED数显芯片、数码管驱动IC、显示LED驱动、数码管LED驱动、LED显示IC、点阵数显驱动、点阵数码管驱动、点阵LED驱动、点阵数显驱动芯片、点阵数显驱动IC、点阵LED驱动芯片、点阵LED驱动IC、LED数显原厂、点阵数码管显示芯片、数码管驱动厂家、数显LED原厂

用 Vue 3 做了一套年会抽奖工具,顺便踩了些坑

去年为了年会抽奖临时做了这个小工具,今年又被点名用同一套方案。
趁着复用的机会,把之前欠下的一些小坑顺手补了一轮,整理完善之后,就决定顺便发出来给大家也能直接用:

  • 在线地址:https://huyikai.github.io/AnnualRaffle/

下面主要想分享两件事:

  • 这套工具大概能做什么;
  • 为啥我要在 UI 里留一个“临时加奖项”的入口,以及实战中这个入口是怎么救场的。

这个“年会抽奖”大概长什么样

不用太多形容词,就按模块简单说一下。

抽奖视觉

  • 中心是一个 3D 标签云,所有参与抽奖的人会在里面旋转。
  • 每个人可以配头像,抽到的时候会同时展示名字和照片。
  • 没配头像的会用默认头像兜底。

奖项相关

  • 默认有常规奖项:一等奖、二等奖、三等奖、幸运奖之类。
  • 每个奖项可以在界面上改中奖人数。
  • 可以配置 预设名单
    • 比如某个特殊奖项,中奖人是提前定好的,只是想在现场走个流程。
  • 支持 全局排除名单(领导不参与抽奖这类场景)。

抽奖过程

  • 底部选择奖项,点击开始,3D 标签云飞快转起来。
  • 有背景音乐、开始音效,可以静音、调音量,设置会保存在浏览器里。
  • 抽完之后会展示本轮中奖人,点击可以删掉,防止误操作。

数据持久化

  • 抽奖配置、名单、结果、音频设置都存在 localStorage
  • 头像文件放在 public/user/,元数据用 IndexedDB 管。

就算现场不小心刷新了浏览器,抽过的结果还在,不至于从头来过。


为什么 UI 里专门留了一个“临时加奖项”

这里有个小故事。

去年那场年会,抽到一半,领导突然来了句:
“再加一个特别奖吧,就现在抽。”

如果奖项是写死在代码里,这种需求基本意味着:

  • 改配置;
  • 重新打包;
  • 重新部署;
  • 祈祷一切顺利。

现场几百个人盯着大屏幕,这种操作就很不现实了。

所以这次我在设计的时候,刻意做了一个分层:

  • 代码里:有一套相对稳定的默认奖项配置,放在 src/config/lottery.ts,方便版本管理和代码 review。
  • UI 里:保留“增加奖项”的交互,可以现场临时加一个奖项、设定人数,用完就算。

实战时这个入口确实救了场:主持人说加奖,我直接在配置弹窗里加了一个新奖项,填好名字和数量,点保存,就可以继续抽了,不需要重启、不需要改代码。

这类现场工具,给业务方留一点“临时操作空间”,比纯粹的“配置优雅”要重要很多。


技术堆栈简单带一下

不细讲实现,只把堆栈和大致结构列一下,方便有兴趣的同学顺着代码看:

  • 基础框架
    • Vue 3 + TypeScript
    • Vite
  • 状态管理
    • Pinia:负责当前奖项、结果列表、配置等全局状态
  • UI / 样式
    • Element Plus:弹窗、表单、按钮等基础组件
    • Tailwind CSS:布局和样式
  • 可视化 / 多媒体
    • TagCanvas:3D 标签云
    • HTML5 Audio:背景音乐和音效
  • 数据存储
    • localStorage:配置、名单、抽奖结果、音频设置
    • IndexedDB:头像元数据

目录拆法比较常规:

  • composables/:抽奖逻辑、3D 标签云封装、音频控制。
  • config/:奖项配置、用户示例数据。
  • helper/:抽奖算法、IndexedDB 操作。
  • components/:配置弹窗、结果展示、公示组件等。

如果你想改 UI 或接入自家系统,大概只需要看这几个目录就够了。


真要用在年会,大概这么搞就够了

假设你也要搞一场年会抽奖,可以按这个顺序来:

1. 拉代码 & 跑起来

pnpm install
pnpm dev

浏览器打开本地地址,先用示例数据跑一遍流程,熟悉一下界面。

2. 换成你们自己的名单

在代码里:

  1. 复制 src/config/user.example.tssrc/config/user.ts
  2. 把里面的用户列表改成你们的人:
export const user: UserItem[] = [
  { key: 1001, name: '张三' },
  { key: 1002, name: '李四' },
  // ...
];

export const excludedUsers: UserItem[] = [
  { key: 9999, name: '某总' }, // 不参与所有奖项
];

不创建 user.ts 也能跑,只是走示例数据。

3. 配奖项

  • 平时:改 src/config/lottery.ts 里的默认奖项即可。
  • 现场:可以直接在“抽奖配置”弹窗里:
    • 调整每个奖项的数量;
    • 给某个奖项加“预设名单”(逗号分隔的用户 ID);
    • 必要时加一个临时奖项。

4. 搞定头像(强烈建议)

视觉氛围基本靠它。

做法:

  1. 准备头像图片,建议 160×160 左右,体积别太大。
  2. 放到 public/user/ 目录。
  3. 命名规则:用户 key + 扩展名,比如 1001.jpg

项目里还有一个批量处理头像的脚本,放在 scripts/ 里,用来把原始照片统一裁成头像,细节可以看 scripts/README.md


批量头像处理脚本:从“真人照片”到统一头像

真实情况是,原始员工照片的尺寸、比例、背景色基本都不一样,直接拿来做头像效果会很怪。

为此我在 scripts/ 目录下写了一套批量头像处理脚本,主要做这些事:

  • 批量裁剪 / 缩放成统一尺寸(默认 160×160)。
  • 统一背景色、统一格式(JPG/PNG)。
  • 提供“快速模式”和“AI 增强模式”,按自己的环境和需求选择。

典型用法:

  1. 把原图放到 scripts/input/
  2. 运行:
pnpm run process-images
  1. scripts/output/ 拿到处理好的头像,拷到 public/user/ 即可。

详细配置(背景色、尺寸、模式开关等)在 scripts/README.md 里都有写。


部署:用 GitHub Pages 省点事

项目现在是放在 GitHub Pages 上的,地址就是:

  • https://huyikai.github.io/AnnualRaffle/

如果你也想这么搞,一般步骤是:

  1. 把代码推到 GitHub。
  2. 用 GitHub Actions 跑一遍:
    • 安装依赖;
    • pnpm build
    • dist/ 发布到 gh-pages 分支。
  3. 在仓库 Settings → Pages 里选对应分支。

配置好之后,后面每次推代码,线上链接会自动更新。


实际使用感受

这套东西在一场不到 200 人的年会上跑了一次,整体体验还可以:

  • 3D 标签云加照片,现场观感比纯文字名单要好不少。
  • 数据持久化避免了“误刷新导致重来一次”的事故。
  • UI 里的“临时加奖项”确实发挥了作用,帮我们兜住了领导临时加奖的情况。

如果你今年也被安排做年会抽奖,可以直接拿这个项目当模板:
先跑一遍 Demo,看下是否符合你们场景,再按自己需求改名单、奖项和 UI 即可。


一点开发过程的小记

这个项目的开发过程里,我是借助了 Cursor 这类辅助工具来提效的:

  • 搭项目、改结构、重构一些逻辑时,用它来帮忙生成样板代码和做局部重构。
  • 这篇文章本身,我也借助了 Cursor 里的 AI 写作辅助,再根据自己的实际场景做了调整。

如果你平时也用 VS Code 类的编辑器,可以尝试一下类似的工作流:
让工具帮你处理重复劳动,把精力放在业务和体验上。


最后:欢迎来 GitHub 点个 Star

项目在线地址我再放一次:

  • 在线 Demo:https://huyikai.github.io/AnnualRaffle/

仓库地址在这里:

  • GitHub 仓库:https://github.com/huyikai/AnnualRaffle

如果你觉得这套年会抽奖还算顺手,或者对你有一点参考价值,欢迎顺手点个 Star。

一文搞懂——React 19到底更新了什么

前言

在2024年12月5日 React19正式版在npm上发布

一、性能提升进入自动化时代——核心更新

告别useMemo、useCallback、React.memo

在React18及以前,为了性能,你得小心翼翼地给函数和计算结果包上 useMemouseCallback,还要盯着那个烦人的依赖数组,写错一个就出 Bug。

但是在React19推出了 React Compiler。它就像一个聪明的助手,在后台自动帮你分析哪些组件需要缓存。你只需要写正常的 JS 代码,性能优化交给编译器,代码量瞬间少了一大截。

下面我们可以看一个例子——

React18的写法

function ProductList({ products, filterTerm }) {
  // 1. 必须手动包裹 useMemo,否则每次渲染都会重新计算
  // 2. 必须死盯着这个 [products, filterTerm] 依赖数组,漏写一个就出 Bug
  const filteredProducts = useMemo(() => {
    console.log("正在执行繁重的过滤逻辑...");
    return products.filter(p => p.name.includes(filterTerm));
  }, [products, filterTerm]); 

  // 3. 传给子组件的函数,必须包裹 useCallback,否则子组件会跟着瞎重绘
  const handlePurchase = useCallback((id) => {
    console.log("购买商品:", id);
  }, []);

  return (
    <ul>
      {filteredProducts.map(p => (
        <Item key={p.id} product={p} onPurchase={handlePurchase} />
      ))}
    </ul>
  );
}

React19的写法

function ProductList({ products, filterTerm }) {
  // 没有任何 Hook,就是纯 JS 逻辑
  // React Compiler 会在编译时自动分析:
  // "只要 products 和 filterTerm 没变,我就直接给上次的结果"
  const filteredProducts = products.filter(p => p.name.includes(filterTerm));

  // 这里的函数也不需要 useCallback
  // 编译器会自动帮你做“函数持久化”,确保子组件不会因为引用变化而重绘
  const handlePurchase = (id) => {
    console.log("购买商品:", id);
  };

  return (
    <ul>
      {filteredProducts.map(p => (
        <Item key={p.id} product={p} onPurchase={handlePurchase} />
      ))}
    </ul>
  );
}

React Compiler的意义

React Compiler的意义远远不止省略了useMemo、useCallback、React.memo. 这是react19更新的核心理念的体现 “通过编译器的力量,抹平框架与原生语言之间的缝隙。”

React 19 的伟大之处不在于它增加了多少新 API,而在于它让‘React 开发者’重新变回了‘JavaScript 开发者’。它承认了人类的大脑不该用来记忆依赖数组,而该用来构建业务逻辑。

对比Vue

谈到React,Vue总是不可避免的一个话题,Vue在缓存方面是怎么做的呢?

Vue 写法

const count = ref(0);

// 你必须明确调用 computed 函数
// 虽然不用写依赖数组,但必须通过 .value 读值
const doubled = computed(() => count.value * 2);

React 18 写法

const [count, setCount] = useState(0);

// 1. 必须手动写 useMemo
// 2. 必须手动维护依赖数组 [count]
const doubled = useMemo(() => {
  return count * 2;
}, [count]);

React 19 写法

const [count, setCount] = useState(0);

// 没有任何框架 API,没有任何包裹函数
// 就像写普通的 JavaScript 一样
const doubled = count * 2;

我们可以看到,在React18之前,Vue的自动依赖追踪是很有优势的,但是在React19,react实现了从落后到反超,那么Vue什么时候会抛弃掉计算属性,也实现自动化优化呢?

React 19 通过“降维打击”,把 Vue 曾经最引以为傲的“自动依赖追踪”变得更透明。

二、React19 的Actions机制——异步状态的“原生化”

useActionState

react18的Actions

在react18假设我们要写一个简单的评论框,逻辑是:点击提交 -> 显示加载中 -> 成功后清空输入框 -> 失败显示错误。那么经典的写法是下面这种

function CommentForm() {
  // 痛点 1:必须手动定义一大堆状态
  const [comment, setComment] = useState("");
  const [isPending, setIsPending] = useState(false);
  const [error, setError] = useState(null);

  const handleSubmit = async (e) => {
    e.preventDefault();
    
    // 痛点 2:手动开启 Loading
    setIsPending(true);
    setError(null);

    try {
      await saveComment(comment); // 异步请求
      setComment(""); // 成功后手动重置
    } catch (e) {
      // 痛点 3:手动捕获并处理错误
      setError(e.message);
    } finally {
      // 痛点 4:手动关闭 Loading
      setIsPending(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <textarea 
        value={comment} 
        onChange={(e) => setComment(e.target.value)} 
      />
      {/* 痛点 5:Loading 状态分散在各处,容易漏掉禁用逻辑 */}
      <button disabled={isPending}>
        {isPending ? "提交中..." : "发布评论"}
      </button>
      {error && <p>{error}</p>}
    </form>
  );
}

可能有小伙伴不理解为什么一定要定义状态isPending和error,我们直接使用不行么?

我们要知道的是react的核心思想是UI=f(date),数据(状态)是驱动UI渲染的最终因素,底层的DOM批量更新也是通过判断状态是否变化来确定的。如果不设置isPending和error就可能出现下面的情况——

你发起一个异步请求,即使后台正在疯狂下载数据,React 的渲染引擎根本不知道这件事。

用户点击了“提交”,页面却没有任何反应(按钮没变灰、没有转圈圈)。用户以为没点着,就会不停地狂点,导致后台收到一堆重复请求。

所以存储 isPending 是为了告诉 React:我现在状态变了,请你赶紧重绘一下 UI。

react18中我们设置的状态到底在做什么

在 React 18 的这种模式下,开发者实际上在充当 “状态协调员”

竞态条件:如果用户连点两次按钮,你需要写额外的逻辑防止第二次请求覆盖第一次。

UI 不同步:如果你忘了在 finally 里关闭 isPending,按钮就永远锁死了。

样板代码冗余:每一个表单、每一个按钮点击,你都要重复写这套 try-catch-finally。

在 React 18 中,异步操作像是一头需要你时刻盯着的猛兽,你必须手动给它关笼子(setLoading)、喂食(handleError)。而在 React 19 中,React 给异步操作装上了‘自动驾驶系统’。你只需要告诉它目的地(Action 函数),它会自动处理起步、巡航和刹车。

react19的优化

在react19中的写法是这样的

function CommentForm() {
  // state 包含返回结果,formAction 是触发函数,isPending 是自动追踪的状态
  const [state, formAction, isPending] = useActionState(
    async (prevState, formData) => {
      const content = formData.get("comment");
      const result = await saveComment(content); 
      return result; // 返回的结果会自动存入 state
    },
    null
  );

  return (
    // 注意:这里不再用 onSubmit,而是用 action
    <form action={formAction}>
      <textarea name="comment" />
      
      {/* isPending 由 React 自动管理,Promise 没结束它就是 true */}
      <button disabled={isPending}>
        {isPending ? "提交中..." : "发布评论"}
      </button>
      
      {state?.error && <p>{state.error}</p>}
    </form>
  );
}

我们可以观察到

  • 消失了: const [loading, setLoading] = useState(false)。

  • 消失了: try { ... } finally { setLoading(false) }。

  • 消失了: 输入框的 value 和 onChange(利用 formData 直接读取)。

  • 消失了: 对“请求是否结束”的手动判断。

为什么以前我们总是在写重复的 useState(false)?因为 React 18 只负责渲染,不负责跟踪你的异步任务。而 React 19 的 Actions 机制,让框架开始‘理解’异步任务的生命周期,从而把开发者从重复的状态定义中解放出来。

useOptimistic

useOptimistic主要解决乐观更新的问题。

react18的乐观更新

function LikeButton() {
  const [likes, setLikes] = useState(100); // 真实数据
  const [error, setError] = useState(null);

  const handleLike = async () => {
    // 1. 备份旧数据(为了失败时回滚)
    const previousLikes = likes;

    // 2. 立即更新 UI(乐观更新)
    setLikes(likes + 1);

    try {
      await updateLikeApi(); // 发送请求
    } catch (e) {
      // 3. 失败处理:手动把数据改回去
      setLikes(previousLikes); 
      setError("更新失败,已回滚");
    }
  };

  return (
    <button onClick={handleLike}>
      {likes} 👍 {error && <span>{error}</span>}
    </button>
  );
}

react19的乐观更新

import { useOptimistic } from 'react';

function LikeButton({ initialLikes }) {
  // 1. 它是基于原始数据的“派生状态”
  const [optimisticLikes, addOptimisticLike] = useOptimistic(
    initialLikes,
    (state, newLike) => state + 1 // 定义如何“乐观地”改变
  );

  async function handleLike() {
    // 2. 瞬间改变 UI
    addOptimisticLike(1);

    // 3. 发送真实请求(配合 Actions)
    await updateLikeApi();
    
    // 【关键点】:函数执行完,React 自动把 UI 同步为服务器回来的真实 initialLikes
    // 开发者不需要写任何“回滚”代码!
  }

  return (
    <button onClick={handleLike}>{optimisticLikes} 👍</button>
  );
}

useOptimistic 引入了一种 ‘阅后即焚’的状态管理模式。它不再需要开发者去写复杂的同步逻辑,而是通过监听异步函数的‘生命周期’,自动完成了从‘幻想’到‘现实’的平滑切换。这种绑定不是通过代码硬连的,而是通过时间维度(异步执行的过程) 自动关联的。

三、ref 不再需要“中间商”:再见 forwardRef

在 React 18 时代,如果你想把 ref 传递给子组件,你必须使用 forwardRef 包裹子组件。这是 React 中最令人反感的“模板代码”之一,写法极其拗口。

React 18 的痛苦写法

const MyInput = forwardRef((props, ref) => {
  return <input {...props} ref={ref} />;
});

React 19 的简洁写法: ref 现在可以像 id、className 一样作为普通的 prop 传递。

function MyInput({ placeholder, ref }) {
  return <input placeholder={placeholder} ref={ref} />;
}

这一改动抹平了 ref 与普通属性之间的差异,组件的结构变得更加扁平、直观。

为什么之前的设计要求使用forwardRef?

1. 语义冲突:谁才是 ref 的主人?

在 React 的早期设计中,ref 的语义非常霸道:它永远指向“当前标签”所对应的直接实例。

  • 对于原生标签(如 <input />):ref 指向真实的 DOM 节点。

  • 对于组件标签(如 <MyComponent />):ref 指向该组件的实例(Instance)。

如果没有 forwardRef,会发生什么? 假设你写了 <MyInput ref={myRef} />

  1. React 引擎会认为:你想拿到 MyInput 这个组件包装盒的引用。

  2. 如果 ref 混在 props 里,子组件内部又把这个 ref 传给了底层的 。

  3. 此时系统就乱了:同一个 ref 到底该指向外层的组件实例,还是里层的 DOM 节点?

为了避免这种指向不明的混乱,React 强制规定:ref 不属于 props,它必须由框架层统一调度。

2. 类组件时代的“实例保护”

在 Hooks 出现之前,React 主要是类组件。

类组件有内部状态、有方法(比如 this.myMethod())。通过 ref 拿到组件实例是非常强大的功能。

如果 React 允许 ref 随 props 传递,子组件可能会不小心修改或覆盖掉父组件传下来的 ref。

forwardRef很大程度上是一份“免责声明”React 官方当时觉得:“转发 Ref 是一件很危险的操作,不能让你随随便便就做了。”于是它强制要求你写 forwardRef。这个复杂的语法实际上是在逼你确认:“我,开发者,现在明确知道我要把父组件的控制权(Ref)交给子组件处理了。如果子组件乱搞,或者指向出错了,我认了。”

将 ref 抽离出来,通过特殊的 forwardRef 接口,实际上是在强迫开发者意识到:“注意,你现在正在把原本属于组件外部的控制权,穿透到组件内部。” 这是一种显式的声明,防止开发者无意中破坏了封装性。

3. 性能与一致性

React 的底层有一个非常高效的 props 比较机制。

props 通常是不可变的纯数据。

ref 是一个可变对象,其 current 属性会随生命周期不断变化。

如果把这个“多动”的 ref 塞进 props,会导致 React 在判断组件是否需要重渲染时,不得不对 ref 做特殊逻辑判断,增加了底层的复杂度。

4.为什么 React 19 现在又敢合回去了?

React 19 之所以能取消 forwardRef,是因为两个环境变了:

函数组件成为绝对主流:函数组件没有“实例”。这意味着当你写 <MyComponent ref={myRef} /> 时,除非你手动转发,否则这个 ref 根本没东西可绑。既然没东西绑,那就不存在“语义冲突”了

编译器变聪明了:React Compiler 现在可以精准地追踪 ref 的去向。既然工具能搞定,就没必要让程序员手写那一层蹩脚的包裹函数了。

forwardRef 的消失,本质上是 React 从‘保护组件实例’转向‘拥抱函数式纯净’的最后一步

过去,React 像一个严厉的家长,担心我们将 ref 乱传导致指向混乱,所以设置了 forwardRef 这个门槛。

如今,在函数组件和编译器的双重护航下,React 终于相信开发者可以处理好 ref 与 props 的关系。这个‘门槛’的拆除,让 React 的代码看起来更像是一段自然的、毫无框架痕迹的 JavaScript 代码。

四、use Hook

React 19 引入的 use 实际上是 Hooks 的 “增强版” 。它最大的突破就是:它可以在条件语句和循环中运行。

注:use并不是一个hook,只是一个API

1.react18的context问题

在react 18中有一个比较大的弊端,那就是hook只能在组件的顶层去定义,而且不可以放在条件判断或者循环中去使用。

假设你有一个权限系统,只有管理员(Admin)才需要读取一个巨大的 PermissionsContext。

function Dashboard({ isAdmin }) {
  // 即使 isAdmin 为 false,你也必须在顶层调用它
  // 这意味着每个普通用户都在订阅这个庞大的上下文
  const permissions = useContext(PermissionsContext); 

  if (!isAdmin) return <p>普通用户界面</p>;

  return <AdminPanel data={permissions} />;
}

性能开销:在 React 中,只要你 useContext,那么当 Context 的值变化时,该组件一定会重新渲染。这意味着成千上万的普通用户,会因为一个他们根本用不到的“管理员权限数据”更新而被迫重新渲染组件。

use 打破了“顶层限制”。你可以把订阅逻辑藏在逻辑判断里:

function Dashboard({ isAdmin }) {
  if (isAdmin) {
    // 只有当程序运行到这里时,React 才会建立组件与 Context 的订阅关系
    const permissions = use(PermissionsContext); 
    return <AdminPanel data={permissions} />;
  }
  return <p>普通用户界面</p>;
}

优化结果:对于非管理员,React 根本不会把这个组件挂载到 PermissionsContext 的监听队列里。这实现了真正的“逻辑按需加载”。

2.直接处理异步 —— 渲染逻辑的“同步化”

React 团队希望开发者读取数据(无论是 Context 还是 Promise)时,能像读取普通变量一样自然,而不是非要包一层 useEffect。

这是 React 19 最具革命性的变化。它让“异步取数据”这件事,在代码观感上变成了“同步取变量”。

1. 传统模式:状态的“中间商”

在以前,数据流是断裂的:

  1. 渲染组件(显示 Loading)。

  2. useEffect 启动(异步拿数据)。

  3. 数据回来,setData(触发二次渲染)。

  4. 再次渲染组件(显示数据)。 开发者必须手动维护这个“中间状态”。

2. React 19 use(Promise) 模式:数据直达

use 让 React 组件具备了“等待”的能力。它不再需要中间状态变量,而是直接深度集成到 React 的 Suspense(悬停机制) 中。

function Message({ messagePromise }) {
  // 1. 如果 Promise 还在 pending,React 暂停当前组件执行,直接跳出
  // 2. 此时,外层的 <Suspense> 捕获并显示 fallback(比如加载动画)
  // 3. 当 Promise 完成(Resolved),React 自动回到这里,把结果给到 content
  const content = use(messagePromise); 
  
  return <p>{content}</p>;
}

我们可以举一个例子来看

(1) React 18 的做法:繁琐的“副作用手动挡”

在 React 19 之前,异步获取数据是一场关于 useEffect、useState 和“时机管理”的博弈。

// React 18 典型写法
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // 每次 userId 变了,都要手动重置状态
    setLoading(true);
    setError(null);

    fetchUser(userId)
      .then((data) => {
        setUser(data); // 手动存入状态
      })
      .catch((err) => {
        setError(err); // 手动处理错误
      })
      .finally(() => {
        setLoading(false); // 手动关闭加载
      });
  }, [userId]); // 必须死盯着依赖数组

  if (loading) return <div>加载中...</div>;
  if (error) return <div>出错了:{error.message}</div>;
  if (!user) return null;

  return <h1>{user.name}</h1>;
}

痛点总结:

  1. 状态冗余:为了显示一个数据,你额外创建了 3 个状态(user, loading, error)。

  2. 流程支离破碎:数据获取逻辑被迫写在 useEffect 里,与 UI 渲染逻辑完全分离。

  3. “瀑布流”困境:这种写法通常会导致组件先渲染出一个空的“壳子”,然后再去加载数据,体验不够流畅。

(2) React 19 的做法:直觉的“渲染即同步”

在 React 19 中,你可以直接在渲染过程中“读取”异步结果。代码看起来就像是同步执行的一样。

// React 19 写法
import { use } from 'react';

function UserProfile({ userPromise }) {
  // 直接“解开”这个 Promise,就像读取一个普通变量
  const user = use(userPromise); 

  // 代码执行到这里时,user 已经是请求成功后的真实数据了
  return <h1>{user.name}</h1>;
}

// 在父组件中使用
function App() {
  const userPromise = fetchUser(123); // 启动异步请求

  return (
    // 错误处理交给外层的 ErrorBoundary
    <ErrorBoundary fallback={<div>出错了</div>}>
      {/* 加载状态交给外层的 Suspense */}
      <Suspense fallback={<div>加载中...</div>}>
        <UserProfile userPromise={userPromise} />
      </Suspense>
    </ErrorBoundary>
  );
}

3. 为什么这个极其重要?

消除竞态条件: 以前你在 useEffect 里发请求,如果请求还没回来 id 就变了,你需要写逻辑去忽略旧请求。而 use 是在渲染流程里的,如果 id 变了,React 会直接处理新的 Promise,旧的会自动被废弃。

组件更纯粹: 组件变成了一个真正的“视图生成器”。它不再是一个管理 Fetch 逻辑的状态机,而是一个 “给它 Promise,它就吐出 UI” 的纯函数。

结语

回顾 React 19 的这些重磅更新,你会发现它们并不是在盲目追求新功能,而是共同在做一个减法——消除“框架带来的心智负担”。

在过去很长一段时间里,React 开发者其实背负着沉重的“框架税”:为了性能,我们得手动管理 useMemo 的依赖数组;为了处理异步,我们得在 useEffect 里编写重复的加载与错误逻辑;为了穿透 ref,我们得忍受 forwardRef 那样拗口的语法。这些逻辑虽然必要,但它们与业务本身无关,更像是为了“迁就” React 的局限性而写的方言。

React 19 的核心命题,就是通过底层的自动化(Compiler)与原生化(Actions / use API),让开发者从一个“React 调优师”重新变回一个“JavaScript 工程师”。 它解决的不仅是代码行数的问题,更是前端开发的第一性原理问题:如果框架足够聪明,开发者就应该只关注 UI 如何响应数据,而不必去操心异步任务的生命周期、复杂的引用缓存或是繁琐的转发逻辑。

Vue 表单修饰符 .lazy:性能优化的秘密武器

Vue 表单修饰符 .lazy:性能优化的秘密武器

在Vue的表单处理中,.lazy修饰符是一个被低估但极其重要的性能优化工具。今天我们来深入探讨它的工作原理、使用场景和最佳实践。

一、.lazy 的核心作用

1.1 基础示例:立即理解差异

<template>
  <div>
    <!-- 没有 .lazy:实时更新 -->
    <input 
      v-model="realtimeText" 
      placeholder="输入时实时更新"
    />
    <p>实时值: {{ realtimeText }}</p>
    
    <!-- 有 .lazy:失焦后更新 -->
    <input 
      v-model.lazy="lazyText" 
      placeholder="失焦后更新"
    />
    <p>懒加载值: {{ lazyText }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      realtimeText: '',
      lazyText: ''
    }
  },
  watch: {
    realtimeText(newVal) {
      console.log('实时输入:', newVal)
      // 每次按键都会触发
    },
    lazyText(newVal) {
      console.log('懒加载输入:', newVal)
      // 只在失焦时触发
    }
  }
}
</script>

1.2 事件机制对比

// Vue 内部处理机制
// 普通 v-model(无 .lazy)
input.addEventListener('input', (e) => {
  // 每次输入事件都触发更新
  this.value = e.target.value
})

// v-model.lazy
input.addEventListener('change', (e) => {
  // 只在 change 事件触发时更新
  // 对于 input:失焦时触发
  // 对于 select/checkbox:选择变化时触发
  this.value = e.target.value
})

二、性能优化深度分析

2.1 性能测试对比

<template>
  <div>
    <h3>性能测试:输入100个字符</h3>
    
    <!-- 测试1:普通绑定 -->
    <div class="test-section">
      <h4>普通 v-model ({{ normalCount }} 次更新)</h4>
      <input v-model="normalText" />
      <p>输入: "{{ normalText }}"</p>
    </div>
    
    <!-- 测试2:.lazy 绑定 -->
    <div class="test-section">
      <h4>v-model.lazy ({{ lazyCount }} 次更新)</h4>
      <input v-model.lazy="lazyText" />
      <p>输入: "{{ lazyText }}"</p>
    </div>
    
    <!-- 测试3:复杂计算场景 -->
    <div class="test-section">
      <h4>复杂计算场景</h4>
      <input 
        v-model="complexText" 
        placeholder="普通绑定 - 输入试试"
      />
      <div v-if="complexText">
        计算耗时操作: {{ heavyComputation(complexText) }}
      </div>
      
      <input 
        v-model.lazy="complexLazyText" 
        placeholder="lazy绑定 - 输入试试"
      />
      <div v-if="complexLazyText">
        计算耗时操作: {{ heavyComputation(complexLazyText) }}
      </div>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      normalText: '',
      lazyText: '',
      complexText: '',
      complexLazyText: '',
      normalCount: 0,
      lazyCount: 0
    }
  },
  watch: {
    normalText() {
      this.normalCount++
    },
    lazyText() {
      this.lazyCount++
    }
  },
  methods: {
    heavyComputation(text) {
      // 模拟耗时计算
      console.time('computation')
      let result = ''
      for (let i = 0; i < 10000; i++) {
        result = text.split('').reverse().join('')
      }
      console.timeEnd('computation')
      return result
    }
  }
}
</script>

2.2 内存和CPU占用对比

// 使用 Performance API 监控
methods: {
  startPerformanceTest() {
    const iterations = 1000
    
    // 测试普通绑定
    console.time('normal binding')
    for (let i = 0; i < iterations; i++) {
      this.normalText = 'test' + i
      // 每次赋值都会触发响应式更新、虚拟DOM diff等
    }
    console.timeEnd('normal binding')
    
    // 测试.lazy绑定
    console.time('lazy binding')
    for (let i = 0; i < iterations; i++) {
      this.lazyText = 'test' + i
      // 只有在change事件时才触发完整更新流程
    }
    console.timeEnd('lazy binding')
  }
}

// 典型结果:
// normal binding: 45.2ms
// lazy binding: 12.7ms (快3.5倍!)

三、实际应用场景

3.1 搜索框优化

<template>
  <div class="search-container">
    <!-- 场景1:实时搜索(不推荐大数据量) -->
    <div class="search-type">
      <h4>实时搜索(普通)</h4>
      <input 
        v-model="searchQuery" 
        placeholder="输入关键词..."
        @input="performSearch"
      />
      <p>API调用次数: {{ apiCalls }}次</p>
      <ul>
        <li v-for="result in searchResults" :key="result.id">
          {{ result.title }}
        </li>
      </ul>
    </div>
    
    <!-- 场景2:失焦搜索(推荐) -->
    <div class="search-type">
      <h4>失焦搜索(.lazy + 防抖)</h4>
      <input 
        v-model.lazy="lazySearchQuery" 
        placeholder="输入后按回车或失焦"
        @keyup.enter="debouncedSearch"
      />
      <p>API调用次数: {{ lazyApiCalls }}次</p>
      <ul>
        <li v-for="result in lazySearchResults" :key="result.id">
          {{ result.title }}
        </li>
      </ul>
    </div>
    
    <!-- 场景3:结合防抖的最佳实践 -->
    <div class="search-type">
      <h4>智能搜索(.lazy + 自动搜索)</h4>
      <input 
        v-model.lazy="smartSearchQuery" 
        placeholder="输入完成后再搜索"
        @change="handleSmartSearch"
      />
      <button @click="handleSmartSearch">搜索</button>
      <p>优化后的API调用</p>
    </div>
  </div>
</template>

<script>
import { debounce } from 'lodash-es'

export default {
  data() {
    return {
      searchQuery: '',
      lazySearchQuery: '',
      smartSearchQuery: '',
      searchResults: [],
      lazySearchResults: [],
      smartSearchResults: [],
      apiCalls: 0,
      lazyApiCalls: 0
    }
  },
  watch: {
    // 普通搜索:每次输入都触发
    searchQuery() {
      this.performSearch()
    },
    // .lazy搜索:只在失焦时触发
    lazySearchQuery() {
      this.performLazySearch()
    }
  },
  created() {
    // 创建防抖函数
    this.debouncedSearch = debounce(this.performLazySearch, 500)
  },
  methods: {
    performSearch() {
      this.apiCalls++
      // 模拟API调用
      fetch(`/api/search?q=${this.searchQuery}`)
        .then(res => res.json())
        .then(data => {
          this.searchResults = data.results
        })
    },
    performLazySearch() {
      this.lazyApiCalls++
      fetch(`/api/search?q=${this.lazySearchQuery}`)
        .then(res => res.json())
        .then(data => {
          this.lazySearchResults = data.results
        })
    },
    handleSmartSearch() {
      // 手动触发搜索逻辑
      this.performSmartSearch()
    },
    async performSmartSearch() {
      if (!this.smartSearchQuery.trim()) return
      
      const response = await fetch(`/api/search?q=${this.smartSearchQuery}`)
      this.smartSearchResults = await response.json()
    }
  }
}
</script>

<style scoped>
.search-container {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 20px;
  padding: 20px;
}

.search-type {
  border: 1px solid #e0e0e0;
  padding: 15px;
  border-radius: 8px;
}
</style>

3.2 表单验证优化

<template>
  <form @submit.prevent="handleSubmit">
    <!-- 普通验证:即时反馈 -->
    <div class="form-group">
      <label>用户名(即时验证):</label>
      <input 
        v-model="username" 
        @input="validateUsername"
        :class="{ 'error': usernameError }"
      />
      <span v-if="usernameError" class="error-message">
        {{ usernameError }}
      </span>
      <p>验证调用: {{ usernameValidations }}次</p>
    </div>
    
    <!-- .lazy验证:失焦时验证 -->
    <div class="form-group">
      <label>邮箱(失焦验证):</label>
      <input 
        v-model.lazy="email" 
        @change="validateEmail"
        :class="{ 'error': emailError }"
      />
      <span v-if="emailError" class="error-message">
        {{ emailError }}
      </span>
      <p>验证调用: {{ emailValidations }}次</p>
    </div>
    
    <!-- 混合策略:即时+失焦 -->
    <div class="form-group">
      <label>密码(混合验证):</label>
      <input 
        type="password"
        v-model="password" 
        @input="validatePasswordBasic"
        @change="validatePasswordAdvanced"
        :class="{ 'error': passwordError }"
      />
      <span v-if="passwordError" class="error-message">
        {{ passwordError }}
      </span>
    </div>
    
    <button type="submit">提交</button>
  </form>
</template>

<script>
export default {
  data() {
    return {
      username: '',
      email: '',
      password: '',
      usernameError: '',
      emailError: '',
      passwordError: '',
      usernameValidations: 0,
      emailValidations: 0
    }
  },
  methods: {
    validateUsername() {
      this.usernameValidations++
      
      // 简单验证
      if (!this.username.trim()) {
        this.usernameError = '用户名不能为空'
      } else if (this.username.length < 3) {
        this.usernameError = '用户名至少3个字符'
      } else {
        this.usernameError = ''
      }
    },
    
    validateEmail() {
      this.emailValidations++
      
      // 复杂邮箱验证
      const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
      if (!this.email) {
        this.emailError = '邮箱不能为空'
      } else if (!emailRegex.test(this.email)) {
        this.emailError = '邮箱格式不正确'
      } else {
        // 异步验证邮箱是否已注册
        this.checkEmailAvailability(this.email)
      }
    },
    
    async checkEmailAvailability(email) {
      try {
        const response = await fetch(`/api/check-email?email=${email}`)
        const { available } = await response.json()
        
        if (!available) {
          this.emailError = '该邮箱已被注册'
        } else {
          this.emailError = ''
        }
      } catch (error) {
        this.emailError = '验证失败,请重试'
      }
    },
    
    validatePasswordBasic() {
      // 即时基础验证
      if (this.password.length < 6) {
        this.passwordError = '密码至少6位'
      } else {
        this.passwordError = ''
      }
    },
    
    validatePasswordAdvanced() {
      // 失焦时高级验证
      if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(this.password)) {
        this.passwordError = '需包含大小写字母和数字'
      }
    },
    
    handleSubmit() {
      // 提交前的最终验证
      this.validateUsername()
      this.validateEmail()
      this.validatePasswordAdvanced()
      
      if (!this.usernameError && !this.emailError && !this.passwordError) {
        console.log('表单提交成功')
        // 提交逻辑...
      }
    }
  }
}
</script>

<style scoped>
.form-group {
  margin-bottom: 20px;
}

.error {
  border-color: #f44336;
  background-color: #ffebee;
}

.error-message {
  color: #f44336;
  font-size: 12px;
  margin-top: 4px;
  display: block;
}
</style>

3.3 大数据量表格编辑

<template>
  <div class="data-table">
    <h3>产品价格表(编辑优化)</h3>
    <table>
      <thead>
        <tr>
          <th>ID</th>
          <th>产品名称</th>
          <th>价格</th>
          <th>库存</th>
          <th>操作</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="product in products" :key="product.id">
          <td>{{ product.id }}</td>
          <td>{{ product.name }}</td>
          <td>
            <!-- 使用 .lazy 避免频繁更新 -->
            <input 
              v-model.lazy="product.price" 
              type="number"
              @change="updateProduct(product)"
            />
          </td>
          <td>
            <input 
              v-model.lazy="product.stock" 
              type="number"
              @change="updateProduct(product)"
            />
          </td>
          <td>
            <button @click="saveProduct(product)">保存</button>
            <span v-if="product.saving">保存中...</span>
            <span v-if="product.saved" class="saved">✓已保存</span>
          </td>
        </tr>
      </tbody>
    </table>
    
    <div class="stats">
      <p>总更新次数: {{ totalUpdates }}</p>
      <p>API调用次数: {{ apiCalls }}</p>
      <p>性能节省: {{ performanceSaving }}%</p>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      products: [],
      totalUpdates: 0,
      apiCalls: 0,
      updatesWithoutLazy: 0 // 模拟没有.lazy时的更新次数
    }
  },
  computed: {
    performanceSaving() {
      if (this.updatesWithoutLazy === 0) return 0
      const saving = ((this.updatesWithoutLazy - this.totalUpdates) / this.updatesWithoutLazy) * 100
      return Math.round(saving)
    }
  },
  created() {
    this.loadProducts()
  },
  methods: {
    async loadProducts() {
      const response = await fetch('/api/products')
      this.products = (await response.json()).map(p => ({
        ...p,
        saving: false,
        saved: false
      }))
    },
    
    updateProduct(product) {
      this.totalUpdates++
      
      // 模拟没有.lazy的情况:每次输入都计数
      this.updatesWithoutLazy += 10 // 假设平均每个字段输入10次
      
      // 标记为需要保存
      product.saved = false
    },
    
    async saveProduct(product) {
      product.saving = true
      this.apiCalls++
      
      try {
        await fetch(`/api/products/${product.id}`, {
          method: 'PUT',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(product)
        })
        product.saved = true
      } catch (error) {
        console.error('保存失败:', error)
      } finally {
        product.saving = false
      }
    }
  }
}
</script>

<style scoped>
.data-table {
  width: 100%;
  overflow-x: auto;
}

table {
  width: 100%;
  border-collapse: collapse;
}

th, td {
  border: 1px solid #ddd;
  padding: 8px;
  text-align: left;
}

th {
  background-color: #f5f5f5;
}

input {
  width: 80px;
  padding: 4px;
}

.saved {
  color: #4caf50;
  margin-left: 8px;
}

.stats {
  margin-top: 20px;
  padding: 10px;
  background-color: #f9f9f9;
  border-radius: 4px;
}
</style>

四、与其他修饰符的组合使用

4.1 .lazy + .trim + .number

<template>
  <div class="combined-modifiers">
    <h3>修饰符组合使用</h3>
    
    <!-- 组合1:.lazy + .trim -->
    <div class="example">
      <label>搜索关键词(自动 trim):</label>
      <input 
        v-model.lazy.trim="searchKeyword" 
        placeholder="输入后自动去除空格"
      />
      <p>值: "{{ searchKeyword }}"</p>
      <p>长度: {{ searchKeyword.length }}</p>
    </div>
    
    <!-- 组合2:.lazy + .number -->
    <div class="example">
      <label>数量(自动转数字):</label>
      <input 
        v-model.lazy.number="quantity" 
        type="number"
        placeholder="输入数字"
      />
      <p>值: {{ quantity }} (类型: {{ typeof quantity }})</p>
    </div>
    
    <!-- 组合3:全部一起用 -->
    <div class="example">
      <label>价格(优化处理):</label>
      <input 
        v-model.lazy.trim.number="price" 
        placeholder="例如: 99.99"
      />
      <p>值: {{ price }} (类型: {{ typeof price }})</p>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      searchKeyword: '',
      quantity: 0,
      price: 0
    }
  },
  watch: {
    searchKeyword(newVal) {
      console.log('搜索关键词变化:', newVal)
    },
    quantity(newVal) {
      console.log('数量变化:', newVal, '类型:', typeof newVal)
    },
    price(newVal) {
      console.log('价格变化:', newVal, '类型:', typeof newVal)
    }
  }
}
</script>

4.2 自定义修饰符

// 创建自定义 .lazy 扩展
const lazyModifier = {
  // 在绑定时添加事件监听
  mounted(el, binding, vnode) {
    const inputHandler = (event) => {
      // 只在特定条件下更新
      if (event.type === 'change' || event.key === 'Enter') {
        binding.value(event.target.value)
      }
    }
    
    el._lazyHandler = inputHandler
    el.addEventListener('input', inputHandler)
    el.addEventListener('change', inputHandler)
    el.addEventListener('keyup', (e) => {
      if (e.key === 'Enter') inputHandler(e)
    })
  },
  
  // 清理
  unmounted(el) {
    el.removeEventListener('input', el._lazyHandler)
    el.removeEventListener('change', el._lazyHandler)
    delete el._lazyHandler
  }
}

// 注册为全局指令
app.directive('lazy', lazyModifier)

// 使用自定义 lazy 指令
<input v-lazy="value" />

五、最佳实践与性能建议

5.1 何时使用 .lazy

// 推荐使用 .lazy 的场景 ✅
const lazyRecommendedScenarios = [
  '表单字段验证(失焦时验证)',
  '搜索框(避免频繁API调用)',
  '大数据量表格编辑',
  '复杂计算依赖的输入',
  '移动端(减少虚拟键盘弹出时的卡顿)',
  '需要与后端同步的字段'
]

// 不建议使用 .lazy 的场景 ❌
const lazyNotRecommendedScenarios = [
  '实时反馈输入(如密码强度检查)',
  '即时搜索建议',
  '字符计数器',
  '需要立即响应的UI(如开关、滑块)',
  '需要实时预览的编辑器'
]

5.2 性能监控代码

// 性能监控装饰器
function withPerformanceMonitor(Component) {
  return {
    extends: Component,
    created() {
      this.inputEvents = 0
      this.updateEvents = 0
      this.performanceLog = []
    },
    methods: {
      logPerformance(eventType) {
        const now = performance.now()
        this.performanceLog.push({
          time: now,
          event: eventType,
          memory: performance.memory?.usedJSHeapSize
        })
        
        // 定期清理日志
        if (this.performanceLog.length > 1000) {
          this.performanceLog = this.performanceLog.slice(-500)
        }
      },
      getPerformanceReport() {
        const events = this.performanceLog.map(log => log.event)
        return {
          totalEvents: events.length,
          inputEvents: events.filter(e => e === 'input').length,
          updateEvents: events.filter(e => e === 'update').length,
          avgTimeBetweenUpdates: this.calculateAvgUpdateTime()
        }
      }
    }
  }
}

// 使用示例
export default withPerformanceMonitor({
  data() {
    return { value: '' }
  },
  watch: {
    value() {
      this.logPerformance('update')
    }
  },
  mounted() {
    this.$el.addEventListener('input', () => {
      this.logPerformance('input')
    })
  }
})

总结

.lazy 修饰符的核心价值:

  1. 性能优化:减少不必要的响应式更新和虚拟DOM diff
  2. 用户体验:避免输入过程中的跳动和卡顿
  3. 资源节省:减少API调用和服务器负载
  4. 控制精度:只在用户完成输入后处理数据

使用准则:

// 决策流程图
function shouldUseLazy(field) {
  if (field.needsRealTimeFeedback) return false
  if (field.triggersHeavyComputation) return true
  if (field.updatesFrequently) return true
  if (field.hasAsyncValidation) return true
  return false
}

// 记住这个口诀:
// "实时反馈不用懒,复杂操作懒优先"
// "表单验证失焦做,搜索优化效果显"

.lazy 修饰符是 Vue 表单处理中的"智能节流阀",合理使用可以显著提升应用性能,特别是在处理复杂表单和大数据场景时。掌握它,让你的 Vue 应用更加流畅高效!

`active-class`:Vue Router 链接组件的激活状态管理

active-class:Vue Router 链接组件的激活状态管理

在 Vue.js 单页应用中,active-class 是一个至关重要但经常被误解的属性。今天我们来彻底搞清楚它是什么、怎么用,以及最佳实践!

一、基础认知:active-class 属于谁?

一句话回答active-classVue Router<router-link> 组件的属性。

<!-- 这是正确的使用方式 -->
<router-link 
  to="/home" 
  active-class="active-link"
>
  首页
</router-link>

错误认知澄清

<!-- 错误!这不是原生HTML属性 -->
<a href="/home" active-class="active">首页</a> ❌

<!-- 错误!这不是普通Vue组件的属性 -->
<button active-class="active">按钮</button> ❌

二、<router-link> 组件深度解析

2.1 基本使用

<template>
  <div id="app">
    <!-- 最简单的用法 -->
    <router-link to="/home">首页</router-link>
    
    <!-- 使用 active-class -->
    <router-link 
      to="/about" 
      active-class="text-red-500 font-bold"
    >
      关于我们
    </router-link>
    
    <!-- 完整配置示例 -->
    <router-link
      to="/products"
      active-class="active-nav-item"
      exact-active-class="exact-active-nav-item"
      class="nav-link"
      :class="{ 'custom-class': isCustom }"
    >
      产品中心
    </router-link>
  </div>
</template>

2.2 <router-link> 的工作机制

// router-link 的内部实现简化
const RouterLink = {
  name: 'RouterLink',
  props: {
    to: { type: [String, Object], required: true },
    activeClass: String,      // active-class 的 prop 名
    exactActiveClass: String, // exact-active-class 的 prop 名
    // ... 其他 props
  },
  
  render() {
    // 1. 解析路由匹配状态
    const isActive = this.$route.path === this.resolvedTo.path
    const isExactActive = this.$route.path === this.resolvedTo.path
    
    // 2. 构建 class 对象
    const classObj = {
      [this.activeClass]: isActive,
      [this.exactActiveClass]: isExactActive,
      // ... 其他 class 逻辑
    }
    
    // 3. 渲染为 <a> 标签
    return h('a', {
      href: this.href,
      class: classObj,
      onClick: this.navigate
    }, this.$slots.default())
  }
}

三、active-class vs exact-active-class

3.1 关键区别

<template>
  <nav>
    <!-- 情况1:普通匹配(active-class) -->
    <router-link 
      to="/dashboard" 
      active-class="bg-blue-100"
    >
      仪表盘
    </router-link>
    <!-- 
      当访问 /dashboard 时:✅ 激活
      当访问 /dashboard/profile 时:✅ 也激活!
      因为 /dashboard/profile 包含 /dashboard
    -->
    
    <!-- 情况2:精确匹配(exact-active-class) -->
    <router-link
      to="/dashboard"
      exact-active-class="bg-blue-500 text-white"
    >
      仪表盘(精确)
    </router-link>
    <!--
      当访问 /dashboard 时:✅ 激活
      当访问 /dashboard/profile 时:❌ 不激活!
      只有路径完全匹配时才激活
    -->
    
    <!-- 情况3:同时使用 -->
    <router-link
      to="/settings"
      active-class="text-blue-500"
      exact-active-class="border-b-2 border-blue-500"
    >
      设置
    </router-link>
    <!--
      访问 /settings:text-blue-500 + border-b-2 border-blue-500
      访问 /settings/account:只有 text-blue-500
    -->
  </nav>
</template>

3.2 实际应用场景

<template>
  <!-- 场景1:面包屑导航 -->
  <div class="breadcrumb">
    <router-link 
      to="/home" 
      active-class="breadcrumb-active"
      exact-active-class="breadcrumb-exact-active"
    >
      首页
    </router-link>
    <span>/</span>
    <router-link 
      to="/products" 
      active-class="breadcrumb-active"
    >
      产品
    </router-link>
    <span v-if="$route.path.includes('/products/')">/</span>
    <router-link 
      v-if="$route.params.id"
      :to="`/products/${$route.params.id}`"
      exact-active-class="breadcrumb-exact-active"
    >
      详情
    </router-link>
  </div>
  
  <!-- 场景2:多级菜单 -->
  <div class="sidebar">
    <div class="menu-group">
      <router-link 
        to="/admin"
        active-class="menu-group-active"
      >
        管理后台
      </router-link>
      <div class="submenu" v-if="$route.path.startsWith('/admin')">
        <router-link 
          to="/admin/users"
          exact-active-class="submenu-active"
        >
          用户管理
        </router-link>
        <router-link 
          to="/admin/products"
          exact-active-class="submenu-active"
        >
          商品管理
        </router-link>
      </div>
    </div>
  </div>
</template>

<style scoped>
.breadcrumb-active {
  color: #666;
}
.breadcrumb-exact-active {
  color: #1890ff;
  font-weight: bold;
}
.menu-group-active {
  background-color: #f0f0f0;
}
.submenu-active {
  color: #1890ff;
  border-left: 3px solid #1890ff;
}
</style>

四、全局配置与最佳实践

4.1 全局配置(推荐方式)

// router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/', component: Home },
    { path: '/about', component: About },
    // ... 其他路由
  ],
  
  // 全局配置 linkActiveClass 和 linkExactActiveClass
  linkActiveClass: 'router-link-active',
  linkExactActiveClass: 'router-link-exact-active'
})

export default router
<template>
  <!-- 使用全局配置 -->
  <router-link to="/home">
    首页 <!-- 自动应用 router-link-active 类 -->
  </router-link>
  
  <!-- 可以覆盖全局配置 -->
  <router-link 
    to="/about"
    active-class="custom-active"
  >
    关于 <!-- 使用 custom-active 而不是 router-link-active -->
  </router-link>
</template>

4.2 最佳实践:CSS 设计

/* 基础样式 */
.router-link {
  padding: 0.5rem 1rem;
  text-decoration: none;
  color: #333;
  transition: all 0.3s ease;
}

/* 激活状态 - 层级指示器 */
.router-link-active {
  color: #1890ff;
  background-color: rgba(24, 144, 255, 0.1);
  position: relative;
}

.router-link-active::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: -2px;
  width: 100%;
  height: 2px;
  background-color: #1890ff;
  transform: scaleX(0.8);
}

/* 精确激活状态 - 当前页面指示器 */
.router-link-exact-active {
  color: #fff;
  background-color: #1890ff;
  font-weight: bold;
}

.router-link-exact-active::after {
  transform: scaleX(1);
  background-color: #fff;
}

/* 配合 Tailwind CSS */
<router-link 
  to="/dashboard"
  class="px-4 py-2 rounded-lg transition-colors"
  active-class="bg-blue-50 text-blue-600"
  exact-active-class="bg-blue-600 text-white"
>
  仪表盘
</router-link>

五、高级用法与技巧

5.1 动态 active-class

<template>
  <div>
    <!-- 根据路由动态设置 -->
    <router-link
      v-for="item in navItems"
      :key="item.path"
      :to="item.path"
      :active-class="getActiveClass(item)"
      :exact-active-class="getExactActiveClass(item)"
    >
      {{ item.name }}
    </router-link>
  </div>
</template>

<script>
export default {
  data() {
    return {
      navItems: [
        { path: '/', name: '首页', icon: 'home', priority: 'high' },
        { path: '/shop', name: '商城', icon: 'shop', priority: 'normal' },
        { path: '/admin', name: '管理', icon: 'admin', priority: 'high' }
      ]
    }
  },
  methods: {
    getActiveClass(item) {
      // 根据优先级返回不同的 active class
      switch(item.priority) {
        case 'high':
          return 'bg-red-50 text-red-600'
        case 'normal':
          return 'bg-gray-50 text-gray-600'
        default:
          return 'active'
      }
    },
    getExactActiveClass(item) {
      // 精确匹配时更强调的样式
      return this.getActiveClass(item) + ' font-bold border-l-4'
    }
  }
}
</script>

5.2 自定义组件封装

<!-- components/SmartLink.vue -->
<template>
  <router-link
    v-bind="$attrs"
    :active-class="computedActiveClass"
    :exact-active-class="computedExactActiveClass"
    v-on="$listeners"
  >
    <!-- 添加图标指示器 -->
    <span v-if="showIcon && isActive" class="active-indicator">▶</span>
    <slot />
  </router-link>
</template>

<script>
export default {
  name: 'SmartLink',
  inheritAttrs: false,
  props: {
    showIcon: {
      type: Boolean,
      default: true
    },
    activeClass: {
      type: String,
      default: 'active'
    },
    exactActiveClass: {
      type: String,
      default: 'exact-active'
    }
  },
  computed: {
    isActive() {
      return this.$route.path.startsWith(this.$attrs.to)
    },
    computedActiveClass() {
      return `${this.activeClass} ${this.isActive ? 'smart-link-active' : ''}`
    },
    computedExactActiveClass() {
      const isExact = this.$route.path === this.$attrs.to
      return `${this.exactActiveClass} ${isExact ? 'smart-link-exact-active' : ''}`
    }
  }
}
</script>

<style scoped>
.active-indicator {
  margin-right: 4px;
  color: #1890ff;
}
.smart-link-active {
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
</style>

5.3 组合式 API 用法

<template>
  <router-link
    ref="linkRef"
    :to="to"
    :class="linkClasses"
    @click="handleClick"
  >
    <slot />
  </router-link>
</template>

<script setup>
import { computed, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'

const props = defineProps({
  to: { type: [String, Object], required: true },
  customActiveClass: String
})

const route = useRoute()
const router = useRouter()
const linkRef = ref(null)

// 计算激活状态
const isActive = computed(() => {
  return route.path.startsWith(typeof props.to === 'string' 
    ? props.to 
    : props.to.path)
})

const isExactActive = computed(() => {
  return route.path === (typeof props.to === 'string' 
    ? props.to 
    : props.to.path)
})

// 动态计算 class
const linkClasses = computed(() => {
  const classes = ['base-link']
  
  if (isActive.value) {
    classes.push(props.customActiveClass || 'active')
  }
  
  if (isExactActive.value) {
    classes.push('exact-active')
  }
  
  return classes
})

// 点击事件处理
const handleClick = (e) => {
  // 可以在这里添加点击分析、权限检查等
  console.log('导航到:', props.to)
}
</script>

六、常见问题与解决方案

6.1 问题:active-class 不生效

<template>
  <!-- 错误示例 -->
  <router-link to="/home" active-class="active">
    首页 <!-- ❌ 可能不生效 -->
  </router-link>
</template>

<!-- 解决方案1:检查 CSS 优先级 -->
<style>
/* ❌ 这可能被覆盖 */
.active {
  color: red;
}

/* ✅ 提高特异性 */
.router-link.active {
  color: red !important; /* 慎用 !important */
}

/* ✅ 更好的方式:使用 Vue 的 scoped */
<style scoped>
/* 这会自动添加 data-v-xxx 属性提高特异性 */
.active {
  color: red;
}
</style>

<!-- 解决方案2:使用全局类名 -->
<router-link 
  to="/home" 
  active-class="global-active-class"
>
  首页
</router-link>

<!-- 在全局 CSS 中 -->
<style>
.global-active-class {
  color: red;
}
</style>

6.2 问题:嵌套路由的激活状态

// 路由配置
const routes = [
  {
    path: '/dashboard',
    component: Dashboard,
    children: [
      { path: '', component: DashboardHome },      // /dashboard
      { path: 'profile', component: Profile },    // /dashboard/profile
      { path: 'settings', component: Settings }   // /dashboard/settings
    ]
  }
]
<template>
  <!-- Dashboard 组件内 -->
  <nav>
    <!-- 问题:访问 /dashboard/profile 时,所有链接都激活? -->
    <router-link to="/dashboard" active-class="active">概览</router-link>
    <router-link to="/dashboard/profile" active-class="active">个人资料</router-link>
    
    <!-- 解决方案:使用 exact 或 exact-active-class -->
    <router-link 
      to="/dashboard" 
      exact-active-class="active"
    >
      概览
    </router-link>
    
    <router-link 
      to="/dashboard/profile" 
      exact-active-class="active"
    >
      个人资料
    </router-link>
    
    <!-- 或者使用嵌套样式 -->
    <router-link 
      to="/dashboard" 
      active-class="parent-active"
      exact-active-class="exact-active"
    >
      概览
    </router-link>
  </nav>
</template>

<style>
.parent-active {
  color: #666; /* 父级激活样式 */
}
.exact-active {
  color: #000; /* 精确激活样式 */
  font-weight: bold;
}
</style>

6.3 问题:与 UI 框架集成

<template>
  <!-- Element Plus 集成 -->
  <el-menu :default-active="$route.path" router>
    <el-menu-item index="/home">
      <router-link 
        to="/home" 
        custom
        v-slot="{ navigate, isActive }"
      >
        <span @click="navigate" :class="{ 'is-active': isActive }">
          首页
        </span>
      </router-link>
    </el-menu-item>
  </el-menu>
  
  <!-- Ant Design Vue 集成 -->
  <a-menu :selected-keys="[$route.path]">
    <a-menu-item key="/home">
      <router-link to="/home">首页</router-link>
    </a-menu-item>
  </a-menu>
  
  <!-- Vuetify 集成 -->
  <v-list nav>
    <v-list-item
      v-for="item in items"
      :key="item.to"
      :to="item.to"
      active-class="v-list-item--active"
    >
      <v-list-item-title>{{ item.title }}</v-list-item-title>
    </v-list-item>
  </v-list>
</template>

七、TypeScript 类型支持

// 全局类型声明
declare module 'vue-router' {
  interface RouterLinkProps {
    // active-class 的类型定义
    activeClass?: string
    exactActiveClass?: string
    // ... 其他属性
  }
}

// 组件中使用
import { RouterLink } from 'vue-router'

// 自定义组件封装
defineProps<{
  to: string | RouteLocationRaw
  activeClass?: string
  exactActiveClass?: string
  customActive?: boolean
}>()

// 组合式API中使用
const linkProps = {
  to: '/dashboard',
  activeClass: computed(() => isActive.value ? 'active' : ''),
  exactActiveClass: 'exact-active'
} as const

总结

active-class 是 Vue Router <router-link> 组件的核心属性,用于管理导航链接的激活状态。记住以下几点:

关键要点:

  1. 属于<router-link> 组件(Vue Router 提供)
  2. 作用:控制路由匹配时的样式类
  3. 配对属性exact-active-class(精确匹配)
  4. 最佳实践:在路由配置中全局设置
  5. CSS 策略:使用 scoped 样式或全局类名

使用原则:

  • 简单导航:使用默认或全局配置
  • 复杂菜单:结合 exact-active-class 区分子路由
  • UI 框架:查看框架文档的集成方案
  • 性能考虑:避免在大量链接上使用复杂计算
<!-- 终极最佳实践示例 -->
<router-link
  :to="{ name: 'Home' }"
  class="nav-link"
  active-class="nav-link--active"
  exact-active-class="nav-link--exact-active"
>
  首页
</router-link>

掌握了 active-class,你就掌握了 Vue 单页应用导航状态管理的精髓!现在就去优化你的导航菜单吧!

Vue Router 参数传递:params vs query 深度解析

Vue Router 参数传递:params vs query 深度解析

在 Vue Router 中传递参数时,你是否曾困惑该用 params 还是 query?这两种看似相似的方式,其实有着本质的区别。今天,我们就来彻底搞清楚它们的差异和使用场景。

一、基础概念对比

1.1 URL 格式差异

// params 方式
http://example.com/user/123
// 对应路由:/user/:id

// query 方式  
http://example.com/user?id=123
// 对应路由:/user

1.2 路由定义方式

// params:必须在路由路径中声明
const routes = [
  {
    path: '/user/:id',          // 必须声明参数名
    name: 'UserDetail',
    component: UserDetail
  },
  {
    path: '/post/:postId/comment/:commentId',  // 多个参数
    component: PostComment
  }
]

// query:无需在路由路径中声明
const routes = [
  {
    path: '/user',              // 直接定义路径
    name: 'User',
    component: User
  },
  {
    path: '/search',            // 不需要声明参数
    component: Search
  }
]

二、核心区别详解

2.1 定义方式与必选性

// params:路径的一部分,通常是必选的
{
  path: '/product/:category/:id',  // 两个必选参数
  component: ProductDetail
}

// 访问时必须提供所有参数
router.push('/product/electronics/123')  // ✅ 正确
router.push('/product/electronics')      // ❌ 错误:缺少id参数

// query:可选的查询字符串
router.push('/search')                    // ✅ 可以不传参数
router.push('/search?keyword=vue')        // ✅ 可以传一个
router.push('/search?keyword=vue&page=2&sort=desc')  // ✅ 可以传多个

2.2 参数获取方式

// 在组件中获取参数
// params 获取方式
export default {
  setup() {
    const route = useRoute()
    // params 是响应式的!
    const userId = computed(() => route.params.id)
    
    // 对于命名路由,还可以这样获取
    const { params } = route
    
    return { userId }
  },
  
  // 选项式API
  mounted() {
    console.log(this.$route.params.id)
  }
}

// query 获取方式
export default {
  setup() {
    const route = useRoute()
    // query 也是响应式的!
    const keyword = computed(() => route.query.keyword)
    const page = computed(() => route.query.page || '1')  // 默认值处理
    
    // 类型转换:query 参数永远是字符串
    const pageNum = computed(() => parseInt(route.query.page) || 1)
    
    return { keyword, pageNum }
  }
}

2.3 编程式导航差异

// params 的多种传递方式
// 方式1:路径字符串
router.push('/user/123')

// 方式2:带params的对象
router.push({ 
  name: 'UserDetail',  // 必须使用命名路由!
  params: { id: 123 }
})

// 方式3:带path和params(不推荐)
router.push({
  path: '/user/123'    // 如果提供了path,params会被忽略!
  // params: { id: 456 }  // ⚠️ 这会被忽略!
})

// query 的传递方式
// 方式1:路径字符串
router.push('/user?id=123')

// 方式2:带query的对象
router.push({
  path: '/user',      // 可以用path
  query: { id: 123 }
})

router.push({
  name: 'User',       // 也可以用name
  query: { id: 123 }
})

三、实际应用场景

3.1 params 的典型场景

// 场景1:资源详情页(RESTful风格)
const routes = [
  {
    path: '/articles/:articleId',
    name: 'ArticleDetail',
    component: ArticleDetail,
    props: true  // 可以将params作为props传递
  }
]

// 组件内使用props接收
export default {
  props: ['articleId'],  // 直接作为props使用
  setup(props) {
    const articleId = ref(props.articleId)
    // ...
  }
}

// 场景2:嵌套路由的参数传递
const routes = [
  {
    path: '/dashboard/:userId',
    component: DashboardLayout,
    children: [
      {
        path: 'profile',  // 实际路径:/dashboard/123/profile
        component: UserProfile
        // 在UserProfile中可以通过 $route.params.userId 访问
      },
      {
        path: 'settings',
        component: UserSettings
      }
    ]
  }
]

// 场景3:多段参数
const routes = [
  {
    path: '/:locale/product/:category/:slug',
    component: ProductPage,
    // 对应URL:/zh-CN/product/electronics/iphone-13
    // params: { locale: 'zh-CN', category: 'electronics', slug: 'iphone-13' }
  }
]

3.2 query 的典型场景

// 场景1:搜索和筛选
// 搜索页面
router.push({
  path: '/search',
  query: {
    q: 'vue 3',
    category: 'technology',
    sort: 'relevance',
    page: '2',
    price_min: '100',
    price_max: '500'
  }
})
// 生成URL: /search?q=vue+3&category=technology&sort=relevance&page=2...

// 场景2:分页和排序
export default {
  methods: {
    goToPage(page) {
      // 保持其他查询参数不变
      const currentQuery = { ...this.$route.query }
      currentQuery.page = page.toString()
      
      this.$router.push({
        query: currentQuery
      })
    },
    
    changeSort(sortBy) {
      this.$router.push({
        query: {
          ...this.$route.query,  // 保留其他参数
          sort: sortBy,
          page: '1'  // 排序变化时回到第一页
        }
      })
    }
  }
}

// 场景3:模态框或临时状态
// 打开用户详情模态框
router.push({
  path: '/users',
  query: { 
    modal: 'user-detail',
    userId: '123'
  }
})

// 在Users组件中
export default {
  watch: {
    '$route.query': {
      handler(query) {
        if (query.modal === 'user-detail') {
          this.showUserDetailModal(query.userId)
        }
      },
      immediate: true
    }
  }
}

四、重要注意事项

4.1 参数持久化问题

// params 的刷新问题
{
  path: '/user/:id',
  name: 'UserDetail'
}

// 从 /user/123 刷新页面
// ✅ 可以正常工作,因为123在URL路径中

// query 的刷新问题
// 从 /user?id=123 刷新页面
// ✅ 也可以正常工作,参数在查询字符串中

// 但是!params 在编程式导航中的问题
router.push({ name: 'UserDetail', params: { id: 123 } })

// 如果用户刷新页面,params 会丢失!
// 因为刷新时浏览器会重新请求URL,而当前URL可能不包含params

// 解决方案1:始终使用完整的URL
router.push(`/user/${id}`)  // 而不是使用params对象

// 解决方案2:结合query作为备份
router.push({
  name: 'UserDetail',
  params: { id: 123 },
  query: { id: 123 }  // 作为备份
})

4.2 类型处理差异

// params 类型处理
router.push({
  name: 'Product',
  params: {
    id: 123,        // 数字
    active: true,   // 布尔值
    tags: ['vue', 'router']  // 数组
  }
})

// 获取时:所有值都会变成字符串!
console.log(this.$route.params.id)      // "123" (字符串)
console.log(this.$route.params.active)  // "true" (字符串)
console.log(this.$route.params.tags)    // "vue,router" (字符串)

// query 类型处理
router.push({
  path: '/product',
  query: {
    id: 123,
    active: true,
    tags: ['vue', 'router']
  }
})
// URL: /product?id=123&active=true&tags=vue&tags=router

// 获取时:query支持数组
console.log(this.$route.query.id)      // "123" (字符串)
console.log(this.$route.query.active)  // "true" (字符串)
console.log(this.$route.query.tags)    // ["vue", "router"] (数组!)

// 最佳实践:类型转换函数
const parseQuery = (query) => ({
  id: parseInt(query.id) || 0,
  active: query.active === 'true',
  tags: Array.isArray(query.tags) ? query.tags : [query.tags].filter(Boolean),
  page: parseInt(query.page) || 1
})

4.3 响应式处理

// 监听参数变化
export default {
  watch: {
    // 监听params变化
    '$route.params.id': {
      handler(newId) {
        this.loadUser(newId)
      },
      immediate: true
    },
    
    // 监听query变化(深度监听)
    '$route.query': {
      handler(newQuery) {
        this.handleSearch(newQuery)
      },
      deep: true,
      immediate: true
    }
  },
  
  // 组合式API方式
  setup() {
    const route = useRoute()
    
    // 监听params中的特定参数
    watch(
      () => route.params.id,
      (newId) => {
        fetchUser(newId)
      },
      { immediate: true }
    )
    
    // 监听整个query对象
    watch(
      () => route.query,
      (newQuery) => {
        performSearch(newQuery)
      },
      { deep: true, immediate: true }
    )
  }
}

五、性能与SEO考虑

5.1 SEO友好性

// params 对SEO更友好
// URL: /products/electronics/laptops
// 搜索引擎更容易理解这个URL的结构层次

// query 对SEO较差
// URL: /products?category=electronics&type=laptops
// 搜索引擎可能不会为每个查询参数组合建立索引

// 最佳实践:
// 核心内容使用params,筛选排序使用query
// /products/:category/:slug?sort=price&order=asc

5.2 服务器配置

// 使用params时需要服务器配置
// 对于Vue的单页应用,需要配置服务器将所有路由指向index.html

// Nginx配置示例
location / {
  try_files $uri $uri/ /index.html;
}

// Apache配置示例
RewriteEngine On
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]

// query不需要特殊配置,因为路径部分还是/index.html

六、实战最佳实践

6.1 参数验证与转换

// 路由级别的参数验证
const routes = [
  {
    path: '/user/:id',
    component: UserDetail,
    props: (route) => ({
      id: validateUserId(route.params.id)
    })
  }
]

// 验证函数
function validateUserId(id) {
  const numId = parseInt(id)
  if (isNaN(numId) || numId <= 0) {
    // 无效ID,重定向到404或列表页
    throw new Error('Invalid user ID')
  }
  return numId
}

// 组件内的参数守卫
export default {
  beforeRouteEnter(to, from, next) {
    const id = parseInt(to.params.id)
    if (isNaN(id)) {
      next({ path: '/users' })  // 重定向
    } else {
      next()
    }
  },
  
  beforeRouteUpdate(to, from, next) {
    // 处理参数变化
    this.userId = parseInt(to.params.id)
    next()
  }
}

6.2 混合使用示例

// 电商网站示例
const routes = [
  {
    path: '/shop/:category',
    name: 'Category',
    component: CategoryPage
    // URL: /shop/electronics?sort=price&page=2
    // params: { category: 'electronics' }
    // query: { sort: 'price', page: '2' }
  },
  {
    path: '/product/:slug/:variant?',  // 变体参数可选
    name: 'Product',
    component: ProductPage
    // URL: /product/iphone-13/blue?showReviews=true
    // params: { slug: 'iphone-13', variant: 'blue' }
    // query: { showReviews: 'true' }
  }
]

// 导航函数示例
export default {
  methods: {
    // 浏览商品分类
    browseCategory(category, options = {}) {
      this.$router.push({
        name: 'Category',
        params: { category },
        query: {
          sort: options.sort || 'popular',
          page: options.page || '1',
          ...options.filters  // 其他筛选条件
        }
      })
    },
    
    // 查看商品详情
    viewProduct(productSlug, variant = null, options = {}) {
      this.$router.push({
        name: 'Product',
        params: { 
          slug: productSlug,
          ...(variant && { variant })  // 条件添加参数
        },
        query: {
          ref: options.referrer,      // 来源跟踪
          showReviews: options.showReviews ? 'true' : undefined
        }
      })
    }
  }
}

七、Vue Router 4 的新特性

7.1 强类型支持(TypeScript)

// 定义路由参数类型
declare module 'vue-router' {
  interface RouteMeta {
    requiresAuth?: boolean
    breadcrumb?: string
  }
  
  interface RouteParams {
    // 全局params类型定义
    id: string
    slug: string
    category: 'electronics' | 'clothing' | 'books'
  }
  
  interface RouteQuery {
    // 全局query类型定义  
    page?: string
    sort?: 'asc' | 'desc'
    q?: string
  }
}

// 在组件中使用类型安全
import { useRoute } from 'vue-router'

export default {
  setup() {
    const route = useRoute()
    
    // TypeScript知道params和query的类型
    const userId = route.params.id  // string
    const page = route.query.page   // string | undefined
    const searchQuery = route.query.q // string | undefined
    
    return { userId, page, searchQuery }
  }
}

7.2 组合式API工具

// 使用useRouter和useRoute
import { useRouter, useRoute } from 'vue-router'

export default {
  setup() {
    const router = useRouter()
    const route = useRoute()
    
    // 编程式导航
    const goToUser = (id) => {
      router.push({
        name: 'UserDetail',
        params: { id },
        query: { tab: 'profile' }
      })
    }
    
    // 响应式参数
    const userId = computed(() => route.params.id)
    const activeTab = computed(() => route.query.tab || 'overview')
    
    // 监听参数变化
    watch(
      () => route.params.id,
      (newId) => {
        fetchUserData(newId)
      }
    )
    
    return { userId, activeTab, goToUser }
  }
}

总结对比表

特性 params query
URL位置 路径的一部分 查询字符串
定义方式 必须在路由中声明 无需声明
必选性 通常是必选的 可选
多个值 不能直接传数组 可以传数组
类型保持 全部转为字符串 数组保持数组,其他转字符串
刷新持久化 路径中,可持久化 查询字符串中,可持久化
SEO友好性 更友好 相对较差
使用场景 资源标识、必要参数 筛选、排序、可选参数
编程式导航 需用name,不能用path namepath都可用

黄金法则

  1. 用 params:当参数是资源标识(如ID、slug)且对URL语义重要时
  2. 用 query:当参数是可选、临时状态或筛选条件时
  3. 混合使用:核心标识用params,附加选项用query
  4. 类型安全:始终进行类型验证和转换
  5. 持久化考虑:重要参数确保刷新后不丢失

记住:params 定义"是什么",query 描述"怎么看"。合理选择,让你的路由既清晰又强大!


思考题:在你的项目中,有没有遇到过因为选错参数传递方式而导致的问题?或者有什么特别的参数处理技巧?欢迎在评论区分享!

百度流式计算开发平台的降本增效之路

导读

在这个高速发展的信息时代,数据洪流已经成为了企业在数字化转型过程中遇到的核心挑战,而流式计算正是应对无界数据流的利器。然而,随着流式技术的普及与发展,其固有的复杂性也日益凸显:

  • 开发门槛高:需要开发者深入掌握事件时间处理、窗口机制、状态管理等复杂概念;

  • 运维成本高:实时系统的容错保障、监控告警与性能调优,往往比离线系统耗费更多人力;

  • 扩展性差:传统流式架构僵化,难以灵活、高效地响应业务的快速迭代与规模增长。

面对这些挑战,业界共识逐渐清晰:流式计算的未来,不应只属于少数专家,而应成为每个团队都能高效使用的通用能力。为此,一种新的破局思路正在兴起——将流式计算与云原生理念深度融合,构建以 Kubernetes 为底座、以开发者体验为中心的 PaaS 化流式开发平台

这样的平台,不仅将底层基础设施的复杂性封装于服务之中,更通过配置化、模板化、自动化的手段,把专家经验转化为平台默认能力,真正实现“让实时计算像搭积木一样简单”。这正是本文所要探讨的核心命题:如何基于云原生技术,打造一个高效、可靠、易用的新一代流式计算 PaaS 平台

01 背景

1.1 流式计算简介

流式计算(Stream Compute)是一种对无界数据流进行实时处理的计算模式。相比于传统的批处理先存储后计算的模型,流式计算会在数据生成时便持续不段的导入、处理和分析,并近乎实时地产出连续的结果。

如果将数据源看做一条奔流不息的“数据河流”:

批处理:修筑水坝,先将河水拦截并蓄水至一定水位线(存储),然后再开闸放水进行计算。这种方式延迟高,但是吞吐量大,适合对时效性不高的海量数据进行离线分析;

  • 流式计算:在河床上安装一套实时监测和过滤系统,对流淌过的每一滴水进行即时分析处理。这种方式延迟极低,使业务能够对瞬息万变的业务场景做出及时反应。

因此,流式计算的核心价值就是时效性,将数据分析这个原本应该出现在“事后复盘”的环节提前到“事中干预”甚至“事前预测”。这在例如实时监控、实时风控、实时推荐等关键业务场景中起到了重要的作用。

1.2 传统流式计算核心挑战

尽管流式计算凭借其时效性高的优点,在企业的业务发展中越来越占据了核心地位,但是由于其复杂性,成了制约企业发展的一个障碍,主要分为开发门槛高、运维成本高、扩展性差三个方面。

1.2.1 开发门槛高

当前市面上主流的流式计算框架(如Flink、Spark Streaming等)以及百度自研的流式计算框架TM,虽然功能强大,但是学习路径异常陡峭。开发者不仅需要了解分布式系统的基本原理,还需要了解:

事件时间与处理时间的处理:如何正确处理乱序事件、延迟数据到达时应该怎么处理等等,这些问题是实现精确业务逻辑的前提,同时也是最容易出错的部分;

  • 复杂的窗口机制:窗口一般分为滚动窗口、滑动窗口和会话窗口,不同窗口的适用场景与配置差异有很大区别,如果选择不当也将影响业务效果;

  • 状态管理机制:有状态计算是流处理的核心问题,而状态的容错、恢复与一致性保障(如Exactly-Once)机制复杂,对开发者的要求也更高。

1.2.2 运维成本高

与离线的批处理不同,流式系统的运维是持续且动态的,这也导致了高昂的运维成本,主要体现在:

容错:在节点故障、网络抖动的情况下,如何保证不重不丢,这就需要复杂的检查点(Checkpoint)机制和保存点(Savepoint)机制;

  • 实时监控与告警:流式系统本身的秒级时效也要求运维团队能够秒级发现并响应问题 ,为了达到这个目标,需要针对于任务延迟、反压(Backpressure)、资源使用率等关键指标配置复杂的监控和告警体系;

  • 持续的性能调优:流式系统的特点是在运行起来之前,没人知道应该怎么样配置资源参数,因为一点点数据量的波动或者业务逻辑变更都可能引发性能瓶颈,造成延迟或者反压等问题。这就需要运维人员持续地针对于系统进行调参,包括并行度、内存资源参数等等。

1.2.3 扩展性差

早期的各类流式计算框架设计上相对僵化,而难以灵活应对当前快速发展的业务需求,其扩展性主要是受制于以下三个方面:

  • 架构耦合度高:计算逻辑与底层资源、存储强耦合,这就导致了升级或迁移时成本较高;

  • 弹性伸缩能力弱:部分流式场景可能会面临突如其来的热点问题,如双十一电商大促,面对可能到来的流量高峰,只能提前估算并扩容,同样地当流量低谷到来时,也将造成资源浪费。在高速迭代的场景下,这样不够灵活的模式越来越捉襟见肘;

  • 业务迭代不敏捷:实际企业业务场景中实时指标或者计算口径的迭代是家常便饭,而现有框架下一个迭代的上线需要复杂的开发、测试、上线流程,无法满足业务快速发展的要求。

1.3 破局之道——构建云原生流式计算PaaS平台

面对开发复杂、运维繁重、扩展受限等痛点,单纯依赖底层框架已难以为继,我们需要一场开发与运维范式的根本性变革。而云原生与PaaS(平台即服务)理念的深度融合,正式引领这场变革的破局点:将流式计算能力封装起来作为云原生PaaS服务,通过平台化手段实现能力下沉、体验上移。

具体而言,平台以Kubernetes为底座,融合配置化开发模型与智能化运行引擎,达成三大转变:

从“写代码”到“配任务”:通过标准化的表单化配置,抽象事件时间、窗口、状态等复杂概念,用户只需声明数据源、处理逻辑与输出目标,即可生成可运行的流式作业,大幅降低开发门槛;

  • 从“人肉运维”到“自动治理”:依托 Kubernetes 的弹性调度、健康探针与 Operator 模式,平台自动完成任务部署、扩缩容、故障恢复与指标采集,将运维复杂度内化于平台;

  • 从“烟囱式架构”到“服务化复用”:通过统一的元数据管理、连接器库与模板市场,实现计算逻辑、数据源、监控策略的跨团队复用,支撑业务敏捷迭代与规模化扩展。

这一 PaaS 化转型,不仅继承了云原生技术在资源效率、可观测性与自动化方面的优势,更将流式计算从“专家专属工具”转变为“全员可用服务”,为企业实时数据能力建设提供了可持续、可复制的基础设施。

02 平台架构总览:云原生PaaS的设计内核

云原生技术(容器化、编排调度、微服务、可观测性)流式计算与PaaS结合提供了 “物理基础”,让平台化能力有了落地的土壤。其核心价值在于实现了流式系统的 “标准化、弹性化、可感知”:

标准化部署:通过 Docker 容器化封装流式任务及其依赖环境,消除 “开发环境与生产环境不一致” 的痛点,同时让任务的部署、迁移、复制变得高效统一 —— 这是智能化调度和弹性扩缩容的前提,确保系统能对任务进行精准操作;

  • 弹性编排调度:基于 Kubernetes(K8s)的编排能力,实现流式任务的自动化部署、调度与生命周期管理。K8s 的 Pod 调度、StatefulSet 状态管理等特性,为流式任务的水平扩缩、故障转移提供了底层支撑,让资源调整变得灵活可控;

  • 全链路可观测:云原生可观测性技术(Prometheus、Grafana、Jaeger 等)构建了 Metrics(指标)、Logs(日志)、Traces(链路追踪)三维监控体系,让流式系统的运行状态 “可视化、可量化、可追溯”。

318f84e872d01dbcfc9851482eacd04c.png 依托云原生的技术,我们构建了四层架构的流式计算基础设施架构,是PaaS落地的技术底座:

  • 硬件资源层:以多地域、多机房的服务器集群为物理支撑,通过分布式部署实现资源规模化与容灾能力,为上层提供算力基础;

  • Kubernetes 编排层:由 K8s Master(集成 API Server、调度器等核心组件)和多节点 K8s Node 组成,承担资源调度、任务编排、弹性扩缩的核心能力,实现流式任务的自动化部署、生命周期管理与资源动态分配;

  • 容器化流式引擎层:以容器化 Pod 形式运行基于厂内自研流式框架TM的算子,通过容器标准化封装消除环境差异,支持水平扩缩容,让计算能力可根据业务流量弹性适配;

  • 可观测性层:通过 Grafana Dashboard 等工具构建全链路监控体系,覆盖指标、日志、链路追踪,为用户实时感知系统状态,及时决策提供了数据支撑。

四层架构的协同,最终实现了**“标准化部署、弹性资源调度、全链路可观测”**的云原生能力,为流式计算的 PaaS 化封装提供了坚实技术底座 —— 将底层复杂的资源管理、引擎调度、监控采集能力下沉,向上层用户暴露 “简单、易用、高效” 的配置化开发接口,完美承接 “降低门槛、简化运维、提升弹性” 的核心目标,让流式计算能力真正以 “服务” 形式交付。

2.1 基石:Kubernetes编排层——资源的智能大脑

Kubernetes 不仅是容器编排引擎,更是整个流式平台的“智能调度中枢”,它是整个平台弹性与自动化的基石。

我们基于K8s实现了流式任务的声明式管理与智能调度。用户提交的任务需求(如所需CPU、内存)被抽象为K8s的定制化资源,而平台的流式任务算子则作为集群内的“自动化运维机器人”,持续监听这些资源状态,并驱动底层执行。其核心价值体现在:

声明式部署与自愈:平台将用户配置的流式任务,自动转换为由Deployment(无状态任务)或StatefulSet(有状态任务,保障Pod名称与存储的稳定)管理的Pod组。当某个Pod因节点故障意外退出时,K8s的控制器会立即在健康节点上重建,通常在秒级内完成故障恢复,实现了从“人工响应”到“自动愈合”的质变。

  • 高效运维与弹性基础:Kubernetes的声明式API与资源模型,为流式任务的高效运维与可控弹性提供了完美基础。平台基于此定义了清晰的资源规格与副本数配置。当业务需要扩缩容时,运维人员只需通过平台更新一个配置值,K8s调度器便会自动、可靠地完成整个实例的扩容或优雅终止流程。这种模式将传统的、易出错的手工部署,转变为一种可审计、可回滚、分钟级内完成的标准化操作,为应对计划内的流量洪峰(如大促)提供了敏捷且可靠的弹性能力。

  • 资源隔离与高效利用:通过K8s的NamespaceResource Quota,平台可以为不同部门或业务线创建逻辑上隔离的资源池,避免相互干扰。同时,K8s调度器能基于节点的实际资源利用率,进行智能装箱(Bin Packing),显著提升集群整体的资源使用效率,降低成本。

综上所述,Kubernetes 在此不仅是“运行环境”,更是实现了 资源调度、弹性控制、高可用保障 三位一体的智能大脑。

2.2 载体:容器化流式引擎层——应用的标准化封装

流式计算的复杂性则很大程度上源于环境依赖于运行时差异,而容器化技术是连接用户逻辑与底层资源的“载体”,是彻底解决这一问题的有效方法:

  • 统一镜像规范:所有流式作业基于标准化基础镜像构建,预装基础环境配置、监控 Agent 和日志采集器,确保“开发、测试、生产”三环境完全一致;

  • 轻量级 Sidecar 模式:每个 Pod 包含主容器(运行流式算子)与 Sidecar 容器(负责日志上报、指标暴露、配置热更新),解耦业务逻辑与平台能力;

  • 资源隔离与限制:通过 K8s 的resources.requests/limits精确控制 CPU、内存分配,避免单个任务资源争抢影响集群稳定性。

容器在此不仅是“打包工具”,更是 标准化交付、安全隔离、敏捷迭代 的核心载体

2.3 视野:可观测性层——系统的透明驾驶舱

对于一个持续运行的实时系统,可观测性如同飞机的驾驶舱仪表盘,是保障其稳定、高效运行的“眼睛”和“直觉”。我们构建了三位一体的可观测性体系:

Metrics(指标)- 系统的脉搏:平台深度集成Prometheus,自动采集每个流式任务Pod的核心性能指标,如**数据吞吐率(records/s)、处理延迟(process_latency)、背压状态(is_backpressured)**以及CPU/内存使用率。通过预置的Grafana仪表盘,运维人员可以一眼掌握全局健康状态,将监控从“黑盒”变为“白盒”。

  • Logs(日志)- 诊断的溯源:所有容器的标准输出与错误日志,通过DaemonSet方式被统一收集、索引(如存入Elasticsearch)。当指标出现异常时,运维人员可以快速关联到对应时间点的详细应用日志,精准定位错误根源,将排障时间从小时级缩短至分钟级。

  • Traces(分布式链路追踪)- 性能的脉络:对于复杂的数据处理流水线,我们通过集成链路追踪,还原一条数据在流式任务DAG中流经各个算子的完整路径和耗时。这使得定位性能瓶颈(例如,是哪部分操作拖慢了整体速度)变得直观而高效。

可观测性在此不仅是“监控工具”,更是 智能决策的数据源泉,为弹性扩缩、用户及时调优提供实时反馈。

16a9b18a198140c82cdc8df55fa361f1.png△ Grafana监控仪表盘

2.4 协同:架构驱动的核心价值闭环

上述三层并非孤立存在,而是通过 “声明 → 执行 → 感知 → 优化” 的闭环紧密协同:

用户通过配置声明业务意图(如“每分钟统计活跃用户”);

  • Kubernetes 编排层将其转化为可调度的 Pod 拓扑,并由容器化引擎执行;

  • 可观测性层持续采集运行数据,形成系统“数字孪生”;

  • 平台基于反馈自动触发弹性扩缩、参数调优或故障恢复,最终兑现 SLA 承诺。

这一闭环,使得平台既能 向下充分利用云原生基础设施的能力,又能 向上为用户提供简单、可靠、高效的流式服务体验。开发门槛、运维成本、扩展性三大痛点,由此在架构层面被系统性化解。

03 配置化开发——从“编码”到“装配”

传统开发模式下,工程师们需要用代码手动地去处理流式计算任务的每一个细节,这是需要复杂和强依赖经验的。而配置化的出现,恰如第一次工业革命的珍妮纺纱机,使工程师们从冗杂的重复工作中释放出来,将“手工作坊”升级生成“现代化生产线”,使流式计算开发变得普惠和平民化。

3.1 从代码到配置:开发模式的范式转移

这场革命最初的表现是开发模式的根本性转变:从命令式(Imperative)转变为声明式(Declarative)的范式转移。

  • 命令式(写代码):开发者需要告诉流式系统**“怎么做”(How),这带来了极大的灵活性,但是同时也伴随着极高的复杂度和学习成本;

  • 声明式(写配置):开发者需要声明**“做什么”(What),而“怎么做”则交由底层引擎去完成。

0f6aeca92db1c38bde53e61c99a19507.png

3.2 隐藏的复杂性:从“专家调优”到“配置默认”

常见的流式系统主要由数据源层、核心计算层、时间容错层、结果输出层这四部分:

数据源层和结果输出层,即数据采集和输出的过程,不在我们此次重点讨论的范围内;

3.2.1 核心计算层

对于核心计算层来说,这里负责了流式作业的主要业务逻辑计算,其中

Import算子——数据接入的“第一入口”

  • 算子特点:作为流式数据进入核心计算层的“门户”,核心职责是实现多种类型数据源的接入和初步格式解析,为后续计算环节提供标准化的数据输入,是保障数据接入稳定性和兼容性的关键。

  • 传统开发模式:需要工程师根据不同的输入数据类型,手动配置响应的链接参数,以进行不同的适配;同时还需要自定义数据解析逻辑,处理不同格式数据的字段映射和类型转换;此外,还需要手动处理连接异常、数据读取重试等问题,避免数据丢失或重复处理。

  • 配置化调优:无需手动编写接入与解析代码,支持多种主流数据格式,如CSV、Parquet、PB等;对于PB格式来说,在预置的标准数据格式模板的基础上,支持上传自定义proto后,通过反射将proto内各个字段映射成便于用户处理的Schema;同时系统内部集成连接容错、自动重试、断点续读机制,保证数据接入的稳定性。

Map/Filter算子——数据预处理的第一个环节

  • 算子特点:最基础、高频的算子,Map 负责对单条数据做结构化转换(如字段格式清洗、维度扩充、单位换算),Filter 则按业务规则筛选数据(如过滤空值、无效订单、非目标场景数据),是所有业务逻辑落地的前置环节;

  • 传统开发模式:开发流式作业时需要工程师手动编写定义转换/筛选逻辑, Map需要逐字段处理数据类型转化,而Filter要精确写明判断条件。除了要保证逻辑精准外,还需要兼顾性能,如复杂字段多层嵌套可能会导致单条数据处理耗时过长,进而引发整条流数据延迟;

  • 配置化调优:无需编写一行代码,通过可视化界面配置流式作业,系统会现针对于用户的数据源进行预处理,将多种多样的格式处理成便于用户直接用Sql语句直接处理的格式,Map 操作支持拖拽算子、上传自定义proto等实现,Filter 可通过配置Sql设置过滤规则。

Aggregate算子——业务指标计算

  • 算子特点:针对于实例内拿到的这一批数据,对数据做聚合计算(如求和、计数、平均值、TopN等),是实现实时业务指标统计的核心算子;

  • 传统开发模式:需要工程师自行定义聚合逻辑,如使用hash map做累加器等,在复杂聚合(如多维度嵌套聚合)的情况下,开发难度大,调试成本高,同时还需要兼顾计算时效和聚合粒度等;

  • 配置化优化:直接写Sql的模式极大降低了开发成本,同时底层采用向量化引擎对列进行操作,相较于传统的行处理模式极大提高了计算效率,提高了时效性。

Sink算子——计算结果的最终出口

  • 算子特点:作为核心计算层的收尾环节,将最终流式作业产出数据输出至下游目标系统,是实时数据价值落地的关键。

  • 传统开发模式:需要工程师手动编写输出代码和配置项,适配下游系统的通信协议与数据格式;同时在Exactly-Once语义要求下,工程师需要协调检查点与Sink算子的事务或幂等写入逻辑,实现难度大;与此同时,批量写入的大小、间隔等参数调优将直接影响吞吐量和端到端延迟。

  • 配置化优化:流式开发平台提供了一套标准化的Sink框架,用户只需要指定落盘的目标系统并配置基础参数,即可实现流式计算结果输出。目前已支持落盘Afs,厂内自研消息队列Bigpipe,以及向量化数据库Doris,未来还将进一步支持Redis、Clickhouse等。

  • 检查点:在配置化场景下,用户仅需要配置检查点存储路径,而触发时机、容错策略、状态分片与恢复等底层复杂逻辑全部交由系统自动托管,提升了流式作业的可用性和易用性。

3.2.2 时间与容错层

时间与容错层是流式计算中“扛风险,保稳定”的核心支撑,水位控制和状态管理两大模块的底层逻辑复杂且易出错,传统开发模式下调优成本高,而配置化将其完全对用户透明,仅在页面上向用户体现为各个计算环节处理进度。

在流式系统中,水位体现了数据的完整性(水位时间之前的所有数据都已就绪)和可见性(当某条数据处理出现故障,水位便不会再退进,问题由此变得可见),作为这么重要的一个概念,水位控制就显得格外重要,往往需要丰富的经验和多次调优才能达到预期的效果。而在配置化的流式平台中,水位的控制对用户基本透明,仅在运维界面体现为各个算子的当前处理进度,在降低了门槛的前提下又保证了水位的数据完整性和可见性两个特点。

而状态管理是Exactly-Once的重要保证,保障了故障恢复时的数据一致性。传统开发模式下,用户需手动设计状态的存储结构(如选择本地内存还是分布式存储)、编写状态序列化 / 反序列化代码、规划状态分片策略以避免单点瓶颈,还要手动处理状态版本冲突、清理过期状态以防止存储膨胀,每一步都依赖对底层存储和分布式系统的深度理解。而在配置化的帮助下,这些技术被完全封装,用户仅需要配置状态存储的路径,其他则完全交由系统实现。

3.3 实践——Push业务在流式计算开发平台的落地

目前,Push业务实时方向优先在流式计算开发平台落地实践,这一决策不仅契合流式计算场景“低延迟、高吞吐、实时处理”的核心特性,更通过创新的开发方式实现了业务价值的高效释放——相较于传统开发模式中“开发-测试-部署-迭代”的冗长链路,新方案大幅简化了流式任务的编排、调试与上线流程,减少了环境适配、依赖冲突等冗余环节,让开发人员能够聚焦核心业务逻辑的迭代优化,无需投入过多精力在底层环境搭建与运维工作上。最终,这一落地策略显著缩短了业务需求从提出到上线的周期,极大提升了业务更新迭代的效率,助力业务快速响应市场变化、迭代产品功能,同时降低了开发与运维成本,为后续在更多云原生、实时计算相关业务场景的规模化推广奠定了坚实基础。

d0a065eea48702679b1166fbabf469a1.png

3.4 降本增效与敏捷迭代

配置化带来的价值是多维且立竿见影的,与我们在背景中讨论过的核心挑战相呼应:

  • 大幅降低开发门槛和人力成本:在有了配置化之后,业务部门想要开发流式任务便不再需要向流式部门提需,只需要经过简单培训即可上手,同时也降低了沟通成本,团队的人力成本得以有效优化;

  • 显著提升运维效率与系统稳定性:标准化的核心优势就是避免了很多人为错误,同时作为模板一定是经过多次试验后的最佳实践,能够保障作业运行的基线性能。同时,统一的交互界面将各个操作接口收口到一个平台上,极大降低了操作成本,版本管理、作业启停变得轻而易举,极大提升了运维效率;

  • 极致优化资源利用:声明式的资源配置让流式系统可以更加灵活地进行资源扩速容调度和优化,避免了资源浪费或瓶颈;

  • 赋能业务敏捷迭代:从前每个简单的迭代(例如将落盘窗口从5分钟修改成15分钟)都需要走开发-测试-上线的繁琐流程,往往会耗时半天至一天,而有了配置化后,仅仅需要在配置界面修改一个参数并重新发布部署即可实现修改,实现了真正的“敏捷开发”,让业务创新快人一步。

04 总结与展望

通过构建基于 Kubernetes 的云原生流式计算 PaaS 平台,我们不仅解决了传统流式系统“开发难、运维重、扩展弱”的三大痛点,更完成了一次开发范式的跃迁——从“手写代码、手动调优”走向“配置驱动、平台兜底”。开发者不再需要深陷于资源调度、状态管理、容错机制等底层复杂性,而是聚焦于业务逻辑本身,真正实现“所想即所得”的流式应用构建体验。这一转变的背后,是平台将多年积累的流式计算最佳实践,以标准化、自动化的方式内嵌于架构之中。无论是时间语义的精准处理,还是 Checkpoint 与 Exactly-Once 的默认保障,平台都在默默承担起“专家角色”,让普通开发者也能轻松驾驭高可靠、高性能的实时计算任务。

展望未来,立足于当前稳固的云原生底座,平台的演进路径清晰可见:

弹性智能化:当前基于可观测层丰富的监控指标,为引入更精细的自动化弹性策略奠定了坚实基础。后续,我们将探索基于自定义监控指标(如水位延迟、CPU使用率、吞吐量波动)的HPA,让资源扩缩容能紧贴真实业务负载,在保障SLA的同时进一步优化成本。

  • 运维自治化:在大模型能力快速发展的当下,基于多年积淀的流式工程经验方案,在RAG技术的加持下构造流式服务运维智能体,实现运维自治化。

  • 体验服务化(Serverless):在配置化开发之上,最终极的体验是让用户完全感知不到底层引擎与基础设施。未来,平台将向流式计算FaaS(Function-as-a-Service)深化,用户只需提交一段业务处理函数或SQL,平台即可自动完成资源调度、引擎选择与任务生命周期管理,实现真正的“按需计算”。

Vue 3 Diff算法革命:比双端比对快在哪里?

当我们还在惊叹Vue 2的diff算法巧妙时,Vue 3已经悄悄完成了一次算法革命。今天,让我们深入源码,看看这个号称"编译时优化"的diff算法到底有多强!

前言:为什么需要优化?

在深入技术细节前,先看一个真实场景:

// 一个常见的列表渲染
const items = [
  { id: 1, name: 'Item 1' },
  { id: 2, name: 'Item 2' },
  // ... 可能有成百上千个
]

// Vue 2 的双端比对在这场景下会遇到瓶颈

Vue 2的双端diff虽然巧妙,但在某些场景下仍有优化空间。Vue 3的目标很明确:减少不必要的虚拟节点比较,让diff更快更智能

一、Vue 2 双端比对:回顾与局限

1.1 经典的双端比对算法

// 简化的双端比对核心逻辑
function patchKeyedChildren(oldChildren, newChildren) {
  let oldStartIdx = 0
  let oldEndIdx = oldChildren.length - 1
  let newStartIdx = 0
  let newEndIdx = newChildren.length - 1
  
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    // 四种情况比较
    // 1. 头头比较
    if (isSameVNode(oldChildren[oldStartIdx], newChildren[newStartIdx])) {
      patch(oldChildren[oldStartIdx], newChildren[newStartIdx])
      oldStartIdx++
      newStartIdx++
    }
    // 2. 尾尾比较
    else if (isSameVNode(oldChildren[oldEndIdx], newChildren[newEndIdx])) {
      patch(oldChildren[oldEndIdx], newChildren[newEndIdx])
      oldEndIdx--
      newEndIdx--
    }
    // 3. 头尾比较
    else if (isSameVNode(oldChildren[oldStartIdx], newChildren[newEndIdx])) {
      patch(oldChildren[oldStartIdx], newChildren[newEndIdx])
      // 移动节点到正确位置
      oldStartIdx++
      newEndIdx--
    }
    // 4. 尾头比较
    else if (isSameVNode(oldChildren[oldEndIdx], newChildren[newStartIdx])) {
      patch(oldChildren[oldEndIdx], newChildren[newStartIdx])
      // 移动节点到正确位置
      oldEndIdx--
      newStartIdx++
    }
    // 5. 都没匹配上,查找中间节点
    else {
      // 复杂的查找和移动逻辑...
    }
  }
}

1.2 双端比对的局限

// 场景1:在头部插入新元素
// 旧: A B C D
// 新: X A B C D

// Vue 2需要:3次节点移动 + 1次插入
// 虽然算法会尽量复用,但仍然需要多次操作

// 场景2:列表完全打乱
// 旧: A B C D E
// 新: E D C B A

// Vue 2需要:O(n²)的时间复杂度查找最优解
// 实际中Vue 2用了key映射优化,但仍有性能开销

主要问题:

  • 总是需要完整遍历新旧节点
  • 移动逻辑相对复杂
  • 无法利用编译时的静态信息

二、Vue 3 Diff算法:编译时+运行时的完美结合

2.1 核心思想:动静分离

Vue 3最大的创新在于编译时分析,标记出哪些节点是静态的、哪些是动态的,从而在运行时跳过不必要的比较。

// Vue 3编译后的渲染函数示例
import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue"

export function render(_ctx, _cache) {
  return (_openBlock(), _createBlock("div", null, [
    _createVNode("h1", null, "静态标题"),  // 静态提升
    _createVNode("p", null, _toDisplayString(_ctx.message), 1 /* TEXT */),
    _createVNode("div", { class: normalizeClass(_ctx.className) }, null, 2 /* CLASS */)
  ]))
}

// 关键数字:Patch Flag
// 1: 文本动态
// 2: class动态  
// 4: style动态
// 8: props动态
// 16: 需要full props diff
// 32: 需要hydrate(SSR)

2.2 新的Diff算法流程

// Vue 3的patchKeyedChildren核心逻辑(简化版)
function patchKeyedChildren(
  oldChildren,
  newChildren,
  container,
  parentAnchor,
  parentComponent
) {
  let i = 0
  const newChildrenLength = newChildren.length
  let oldChildrenEnd = oldChildren.length - 1
  let newChildrenEnd = newChildrenLength - 1
  
  // 1. 从前向后扫描(预处理)
  while (i <= oldChildrenEnd && i <= newChildrenEnd) {
    const oldVNode = oldChildren[i]
    const newVNode = normalizeVNode(newChildren[i])
    if (isSameVNodeType(oldVNode, newVNode)) {
      patch(oldVNode, newVNode, container, null, parentComponent)
    } else {
      break
    }
    i++
  }
  
  // 2. 从后向前扫描(预处理)
  while (i <= oldChildrenEnd && i <= newChildrenEnd) {
    const oldVNode = oldChildren[oldChildrenEnd]
    const newVNode = normalizeVNode(newChildren[newChildrenEnd])
    if (isSameVNodeType(oldVNode, newVNode)) {
      patch(oldVNode, newVNode, container, null, parentComponent)
    } else {
      break
    }
    oldChildrenEnd--
    newChildrenEnd--
  }
  
  // 3. 特殊情况的快速处理
  if (i > oldChildrenEnd) {
    // 只有新增节点
    if (i <= newChildrenEnd) {
      mountChildren(newChildren, container, parentAnchor, parentComponent, i, newChildrenEnd)
    }
  } else if (i > newChildrenEnd) {
    // 只有删除节点
    unmountChildren(oldChildren, parentComponent, i, oldChildrenEnd)
  } else {
    // 4. 复杂情况:建立key到索引的映射
    const keyToNewIndexMap = new Map()
    for (let j = i; j <= newChildrenEnd; j++) {
      const newChild = normalizeVNode(newChildren[j])
      if (newChild.key != null) {
        keyToNewIndexMap.set(newChild.key, j)
      }
    }
    
    // 5. 移动和挂载新节点
    // 使用最长递增子序列算法优化移动次数
    const increasingNewIndexSequence = getSequence(newIndices)
    let j = increasingNewIndexSequence.length - 1
    for (let k = toBePatched - 1; k >= 0; k--) {
      // 智能移动逻辑...
    }
  }
}

2.3 最长递增子序列(LIS)算法

这是Vue 3 diff算法的"杀手锏":

// 最长递增子序列实现
function getSequence(arr) {
  const p = arr.slice()  // 保存前驱索引
  const result = [0]     // 结果索引数组
  
  for (let i = 0; i < arr.length; i++) {
    const arrI = arr[i]
    if (arrI !== 0) {
      const j = result[result.length - 1]
      if (arr[j] < arrI) {
        p[i] = j
        result.push(i)
        continue
      }
      
      // 二分查找替换位置
      let left = 0
      let right = result.length - 1
      while (left < right) {
        const mid = (left + right) >> 1
        if (arr[result[mid]] < arrI) {
          left = mid + 1
        } else {
          right = mid
        }
      }
      
      if (arrI < arr[result[left]]) {
        if (left > 0) {
          p[i] = result[left - 1]
        }
        result[left] = i
      }
    }
  }
  
  // 回溯构建最长序列
  let u = result.length
  let v = result[u - 1]
  while (u-- > 0) {
    result[u] = v
    v = p[v]
  }
  
  return result
}

// 实际应用:找出不需要移动的节点
// 旧索引: [0, 1, 2, 3, 4]
// 新索引: [4, 0, 1, 2, 3] 
// LIS结果: [1, 2, 3] → 节点0、1、2保持相对顺序,只需移动节点4

三、性能对比:实测数据说话

3.1 基准测试

// 测试场景:1000个节点的列表更新
const testCases = [
  {
    name: '头部插入',
    old: Array.from({length: 1000}, (_, i) => i),
    new: [-1, ...Array.from({length: 1000}, (_, i) => i)]
  },
  {
    name: '尾部插入', 
    old: Array.from({length: 1000}, (_, i) => i),
    new: [...Array.from({length: 1000}, (_, i) => i), 1000]
  },
  {
    name: '中间插入',
    old: Array.from({length: 1000}, (_, i) => i),
    new: [...Array.from({length: 500}, (_, i) => i), 
          999, 
          ...Array.from({length: 500}, (_, i) => i + 500)]
  },
  {
    name: '顺序反转',
    old: Array.from({length: 1000}, (_, i) => i),
    new: Array.from({length: 1000}, (_, i) => 999 - i)
  }
]

// 测试结果:
// 头部插入: Vue 2 ≈ 15ms, Vue 3 ≈ 3ms (快5倍)
// 尾部插入: Vue 2 ≈ 8ms, Vue 3 ≈ 2ms (快4倍)  
// 中间插入: Vue 2 ≈ 22ms, Vue 3 ≈ 5ms (快4.4倍)
// 顺序反转: Vue 2 ≈ 35ms, Vue 3 ≈ 8ms (快4.4倍)

3.2 内存占用对比

// 虚拟节点数据结构对比
// Vue 2的VNode
{
  tag: 'div',
  data: { /* 所有属性,无论静态动态 */ },
  children: [ /* 所有子节点 */ ],
  elm: /* DOM元素 */,
  context: /* 组件实例 */,
  // ... 还有其他10+个属性
}

// Vue 3的VNode  
{
  type: 'div',
  props: { /* 仅动态属性 */ },
  children: [ /* 仅动态子节点或静态提升引用 */ ],
  el: /* DOM元素 */,
  // 更扁平,属性更少
  shapeFlag: 16, // 形状标志,标识节点类型
  patchFlag: 8,  // 补丁标志,标识哪些需要更新
  dynamicChildren: [ /* 仅动态子节点 */ ] // 🎯 关键优化!
}

// 内存节省:平均减少30%-50%!

四、关键技术点深度解析

4.1 Block Tree 的概念

// Block: 一个包含动态子节点的虚拟节点
const block = {
  type: 'div',
  children: [
    _hoisted_1,  // 静态节点1(已提升)
    _createVNode("p", null, _ctx.dynamicText, 1 /* TEXT */),
    _hoisted_2,  // 静态节点2(已提升)
    _createVNode("button", { onClick: _ctx.handleClick }, "点击", 8 /* PROPS */)
  ],
  dynamicChildren: [  // 🎯 只包含动态子节点!
    // 只有索引1和3的节点在这里
    _createVNode("p", null, _ctx.dynamicText, 1 /* TEXT */),
    _createVNode("button", { onClick: _ctx.handleClick }, "点击", 8 /* PROPS */)
  ]
}

// 更新时只比较dynamicChildren!
// 静态节点完全跳过比较

4.2 Patch Flags 的威力

// 编译时分析,运行时优化
const vnode = _createVNode("div", {
  id: _ctx.id,                    // 动态属性
  class: normalizeClass(_ctx.className), // 动态class
  style: normalizeStyle(_ctx.style),    // 动态style
  onClick: _ctx.handleClick       // 动态事件
}, [
  _createVNode("span", null, _ctx.text) // 动态文本
])

// 编译后生成patchFlag
const patchFlag = 1 /* TEXT */ | 
                  2 /* CLASS */ | 
                  4 /* STYLE */ | 
                  8 /* PROPS */ |
                  16 /* FULL_PROPS */

// 运行时根据patchFlag快速判断更新策略
if (patchFlag & PatchFlags.CLASS) {
  // 只更新class
  hostPatchProp(el, 'class', null, newProps.class)
}
if (patchFlag & PatchFlags.STYLE) {
  // 只更新style
  hostPatchProp(el, 'style', null, newProps.style)
}
// 不需要全量比较所有props!

4.3 静态提升(Hoisting)

// 编译前
<template>
  <div>
    <h1>欢迎来到Vue 3</h1>  <!-- 静态 -->
    <p>{{ message }}</p>    <!-- 动态 -->
    <footer>版权所有 © 2024</footer>  <!-- 静态 -->
  </div>
</template>

// 编译后
const _hoisted_1 = /*#__PURE__*/_createVNode("h1", null, "欢迎来到Vue 3", -1 /* HOISTED */)
const _hoisted_2 = /*#__PURE__*/_createVNode("footer", null, "版权所有 © 2024", -1 /* HOISTED */)

function render(_ctx) {
  return (_openBlock(), _createBlock("div", null, [
    _hoisted_1,  // 直接引用,不参与diff
    _createVNode("p", null, _toDisplayString(_ctx.message), 1 /* TEXT */),
    _hoisted_2   // 直接引用,不参与diff
  ]))
}

// 效果:每次更新跳过2个静态节点比较

五、实际开发中的优化建议

5.1 合理使用Key

// 反例:使用索引作为key(Vue 3中仍然不推荐)
<template v-for="(item, index) in items" :key="index">
  <!-- 当列表顺序变化时,会导致不必要的重新渲染 -->
</template>

// 正例:使用唯一标识
<template v-for="item in items" :key="item.id">
  <!-- Vue 3能更高效地复用节点 -->
</template>

// 特殊场景:没有id时
<template v-for="item in items" :key="item">
  <!-- 如果item是原始值,也可以直接使用 -->
</template>

5.2 利用编译时优化

// 优化前:所有属性都绑定
<div :class="className" :style="style" @click="handleClick">
  {{ text }}
</div>

// 优化后:静态和动态分离
<div class="static-class" :class="dynamicClass" 
     :style="dynamicStyle" @click="handleClick">
  <span class="static-text">标题:</span>
  {{ dynamicText }}
</div>

// 编译结果差异:
// 优化前:patchFlag = 31 (几乎全量比较)
// 优化后:patchFlag = 11 (只比较class、style、props)

5.3 避免不必要的响应式

// 反例:所有数据都是响应式的
setup() {
  const config = reactive({
    apiUrl: 'https://api.example.com',
    maxRetries: 3,
    timeout: 5000
  })
  
  // config在组件生命周期内不会改变,不需要响应式!
  
  return { config }
}

// 正例:只对需要变化的数据使用响应式
setup() {
  const staticConfig = {
    apiUrl: 'https://api.example.com',
    maxRetries: 3, 
    timeout: 5000
  }
  
  const dynamicData = reactive({
    loading: false,
    items: []
  })
  
  return { staticConfig, dynamicData }
}

六、源码学习路径建议

如果你想深入理解Vue 3的diff算法,建议按以下顺序阅读源码:

  1. packages/runtime-core/src/renderer.ts - 核心渲染逻辑
  2. packages/runtime-core/src/vnode.ts - 虚拟节点定义
  3. packages/compiler-core/src/transforms/ - 编译时变换
  4. packages/reactivity/src/effect.ts - 响应式与更新调度

关键函数:

  • patch() - 核心打补丁函数
  • patchKeyedChildren() - 新的diff算法实现
  • getSequence() - 最长递增子序列算法

总结

Vue 3的diff算法革新不是简单的"算法优化",而是编译时与运行时协同优化的典范

维度 Vue 2 双端比对 Vue 3 快速diff
核心思想 运行时优化 编译时+运行时协同
时间复杂度 O(n) ~ O(n²) 接近 O(n)
空间复杂度 较高 较低(动态子树)
静态处理 无特别优化 静态提升,完全跳过
移动策略 双端查找 LIS算法,最小化移动
更新粒度 组件/虚拟节点级 属性级(patchFlag)
内存占用 较高 减少30%-50%

Vue 3 diff算法的三大革命性改进:

  1. 动静分离:通过编译时分析,静态内容完全不参与diff
  2. 靶向更新:通过patchFlag实现属性级精准更新
  3. 智能移动:通过LIS算法最小化DOM操作

正如尤雨溪在RFC中说的:"我们不再追求极致的运行时算法优化,而是将一部分工作转移到编译时,让运行时更轻量、更高效。"

这种思路的转变,不仅带来了性能的巨大提升,更重要的是为未来的优化打开了更广阔的空间。

让 Vant 弹出层适配 Uniapp Webview 返回键

问题背景

在 UniApp Webview 中使用 Vant 组件库时,返回键的行为往往不符合用户预期:

  • 当 Popup、Dialog、ActionSheet 等弹出层打开时,用户按下返回键会直接返回上一页,而不是关闭弹出层
  • 多层弹出层叠加时,无法按层级顺序依次关闭
  • Vant 内置的 closeOnPopstate 仅会在页面回退时自动关闭弹窗,而不会阻止页面回退

这导致用户体验与原生应用存在明显差距。

解决方案

@vue-spark/back-handler 提供了基于栈的返回键处理机制,可以与 Vant 组件无缝集成,让弹出层正确响应 UniApp 的返回键事件。

核心思路:将每个需要响应返回键的弹出层注册到全局栈中,按后进先出的顺序处理返回事件。

使用方式

1. 安装依赖

npm install @vue-spark/back-handler

2. 初始化插件(UniApp 适配)

// main.ts
import { BackHandler } from '@vue-spark/back-handler'
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

createApp(App)
  .use((app) => {
    const routerHistory = router.options.history

    let initialPosition = 0
    const hasRouteHistory = () => {
      // 当 vue-router 内部记录的位置不是初始位置时认为还存在历史记录
      return routerHistory.state.position !== initialPosition
    }

    router.isReady().then(() => {
      // 记录初始位置
      initialPosition = routerHistory.state.position as number

      router.afterEach(() => {
        // 每次页面变更后通知 uniapp 是否需要阻止返回键默认行为
        uni.postMessage({
          type: 'preventBackPress',
          data: hasRouteHistory(),
        })
      })
    })

    // 注册插件
    BackHandler.install(app, {
      // 每次增加栈记录时通知 uniapp 阻止返回键默认行为
      onPush() {
        uni.postMessage({
          type: 'preventBackPress',
          data: true,
        })
      },

      // 每次移除栈记录时通知 uniapp 是否阻止返回键默认行为
      onRemove() {
        uni.postMessage({
          type: 'preventBackPress',
          data: BackHandler.stack.length > 0 || hasRouteHistory(),
        })
      },

      // 栈为空时尝试页面回退
      fallback() {
        hasRouteHistory() && router.back()
      },

      // 这里绑定 uniapp webview 触发的 backbutton 事件
      bind(handler) {
        window.addEventListener('uni:backbutton', handler)
      },
    })
  })

  // 在这里注册 router
  .use(router)

  // 挂载应用
  .mount('#app')

3. 适配 Vant Popup

通过扩展 Popup 的 setup 函数,让所有基于 Popup 的组件(ActionSheet、ShareSheet、Picker 等)都支持返回键关闭:

import { useBackHandler } from '@vue-spark/back-handler'
import { Dialog, Popup } from 'vant'
import { callInterceptor } from 'vant/es/utils'
import { getCurrentInstance, watch } from 'vue'

const { setup } = Popup

// 变更 closeOnPopstate 默认值为 true
Popup.props.closeOnPopstate = {
  type: Boolean,
  default: true,
}

Popup.setup = (props, ctx) => {
  const { emit } = ctx
  const vm = getCurrentInstance()!

  // Dialog 组件基于 Popup,这里需要排除,否则会重复注册
  if (vm.parent?.type !== Dialog) {
    const close = () => {
      return new Promise<void>((resolve, reject) => {
        if (!props.show) {
          return resolve()
        }

        callInterceptor(props.beforeClose, {
          done() {
            emit('close')
            emit('update:show', false)
            resolve()
          },
          canceled() {
            reject(new Error('canceled'))
          },
        })
      })
    }

    const { push, remove } = useBackHandler(
      () => props.show,
      // closeOnPopstate 用于控制是否响应返回键
      () => !!props.closeOnPopstate && close(),
    )

    watch(
      () => props.show,
      (value) => (value ? push() : remove()),
      { immediate: true, flush: 'sync' },
    )
  }

  return setup!(props, ctx)
}

4. 适配 Vant Dialog

Dialog 需要单独适配,因为它基于 Popup 但有独立的关闭逻辑:

import { useBackHandler } from '@vue-spark/back-handler'
import { Dialog, showLoadingToast } from 'vant'
import { callInterceptor } from 'vant/es/utils'
import { watch } from 'vue'

// Dialog 的 closeOnPopstate 默认为 true,可以不修改默认值
const { setup } = Dialog

Dialog.setup = (props, ctx) => {
  const { emit } = ctx
  const updateShow = (value: boolean) => emit('update:show', value)

  const close = (action: 'cancel') => {
    updateShow(false)
    props.callback?.(action)
  }

  const getActionHandler = (action: 'cancel') => () => {
    return new Promise<void>((resolve, reject) => {
      if (!props.show) {
        return resolve()
      }

      emit(action)

      if (props.beforeClose) {
        const toast = showLoadingToast({})
        callInterceptor(props.beforeClose, {
          args: [action],
          done() {
            close(action)
            toast.close()
            resolve()
          },
          canceled() {
            toast.close()
            reject(new Error('canceled'))
          },
        })
      } else {
        close(action)
        resolve()
      }
    })
  }

  const { push, remove } = useBackHandler(
    () => props.show,
    // closeOnPopstate 用于控制是否响应返回键
    () => !!props.closeOnPopstate && getActionHandler('cancel')(),
  )

  watch(
    () => props.show,
    (value) => (value ? push() : remove()),
    { immediate: true, flush: 'sync' },
  )

  return setup!(props, ctx)
}

效果

完成上述配置后:

  • Popup、ActionSheet、ShareSheet、Picker、Dialog 等弹出层在打开时,按返回键会关闭弹出层而不是退出页面
  • 多层弹出层会按打开顺序的逆序依次关闭
  • 支持 beforeClose 拦截器进行异步确认

相关链接

深入理解MessageChannel:JS双向通信的高效解决方案

===

在前端开发中,跨执行环境的通信是常见需求——比如主线程与Web Worker间的数据交互、iframe与父页面的消息传递等。传统的全局事件监听方式虽然简单,但在复杂场景下容易出现消息冲突、性能低下等问题。MessageChannel作为JavaScript原生提供的双向通信API,为这类场景提供了轻量、高效的解决方案。本文将深入解析MessageChannel的工作原理、核心优势及实际应用。

一、MessageChannel核心概念:建立专属通信通道

什么是MessageChannel?

MessageChannel是浏览器提供的原生API,用于在两个独立的JavaScript执行环境之间建立专属的双向通信通道。每个通道包含两个互相关联的端口(port1port2),形成一个完整的通信闭环。

核心特性解析

  • 双向通信:两个端口均可发送和接收消息,实现真正的双向对话

  • 独立通道:每个通道都是隔离的,不同通道互不干扰,避免消息污染

  • 跨环境支持:可在主线程、Web Worker、iframe、SharedWorker等任意组合间建立连接

  • 异步无阻塞:基于事件机制,不会阻塞主线程执行

  • 所有权转移:端口可以安全地转移给其他执行环境

基础工作流程

// 1. 创建通道实例
const channel = new MessageChannel();
const { port1, port2 } = channel;

// 2. 将一个端口传递给目标环境
target.postMessage('init', '*', [port2]);

// 3. 监听端口消息
port1.onmessage = (event) => {
  console.log('收到消息:', event.data);
};

// 4. 发送消息
port1.postMessage('Hello from port1');

// 5. 关闭端口(可选)
// port1.close();

二、MessageChannel典型使用场景

1. 主线程与Web Worker的高效通信

Web Worker常用于处理计算密集型任务,但传统的worker.postMessage()方式存在性能瓶颈。每次通信都需要对数据进行序列化/反序列化(结构化克隆),频繁通信时开销明显。

MessageChannel解决方案

  • 建立专属通道,减少序列化开销

  • 实现精准的"一对一"通信

  • 支持复杂的数据传输(如ArrayBuffer、ImageBitmap等可转移对象)

2. 父页面与iframe的安全通信

window.postMessage虽然能实现跨域通信,但存在安全隐患:

  • 全局监听可能被恶意页面劫持

  • 多iframe场景下消息来源难以区分

  • 缺乏消息确认机制

MessageChannel优势

// 为每个iframe创建独立通道
const iframeChannels = new Map();

function connectToIframe(iframeId) {
  const channel = new MessageChannel();
  iframeChannels.set(iframeId, channel.port1);
  // 仅目标iframe能接收到端口
  document.getElementById(iframeId).contentWindow
    .postMessage('channel-init', '*', [channel.port2]);
}

3. SharedWorker多页面通信管理

当多个页面共享同一个Worker时,MessageChannel能为每个页面建立独立的通信链路,避免消息广播带来的混乱。

4. 异步任务解耦与封装

在微前端、插件化架构中,可通过MessageChannel将独立模块封装在隔离环境中:

// 创建数据处理专用Worker
class DataProcessor {
  constructor() {
    this.channel = new MessageChannel();
    this.worker = new Worker('processor.js');
    this.setupChannel();
  }
  
  async process(data) {
    return new Promise((resolve) => {
      this.channel.port1.onmessage = (e) => resolve(e.data);
      this.channel.port1.postMessage(data);
    });
  }
}

5. 跨标签页通信优化

结合BroadcastChannel实现跨标签页的高效通信:

  1. 使用BroadcastChannel广播通道建立请求

  2. 通过MessageChannel建立专属数据通道

  3. 进行高频或大数据量的传输

三、实战示例:核心场景代码实现

示例1:主线程与Web Worker的双向通信

主线程代码(main.js)

class WorkerManager {
  constructor(workerUrl) {
    this.worker = new Worker(workerUrl);
    this.channel = new MessageChannel();
    this.initChannel();
  }
  
  initChannel() {
    // 将port2传递给Worker
    this.worker.postMessage(
      { type: 'INIT_CHANNEL' },
      [this.channel.port2]
    );
    
    // 设置消息监听
    this.channel.port1.onmessage = this.handleMessage.bind(this);
    this.channel.port1.onmessageerror = this.handleError.bind(this);
  }
  
  handleMessage(event) {
    const { type, data, id } = event.data;
    
    if (type === 'RESULT') {
      // 处理Worker返回的结果
      this.pendingRequests.get(id)?.resolve(data);
      this.pendingRequests.delete(id);
    }
  }
  
  async sendTask(taskData) {
    const taskId = Date.now() + Math.random();
    
    return new Promise((resolve, reject) => {
      this.pendingRequests.set(taskId, { resolve, reject });
      
      this.channel.port1.postMessage({
        type: 'EXECUTE_TASK',
        id: taskId,
        data: taskData
      });
      
      // 设置超时
      setTimeout(() => {
        if (this.pendingRequests.has(taskId)) {
          reject(new Error('Worker timeout'));
          this.pendingRequests.delete(taskId);
        }
      }, 5000);
    });
  }
}

Worker代码(worker.js)

let communicationPort = null;

// 监听主线程初始化
self.onmessage = function(event) {
  const { type, ports } = event.data;
  
  if (type === 'INIT_CHANNEL' && ports[0]) {
    communicationPort = ports[0];
    
    communicationPort.onmessage = async function(event) {
      const { type, id, data } = event.data;
      
      if (type === 'EXECUTE_TASK') {
        try {
          // 执行计算密集型任务
          const result = await processData(data);
          
          // 返回结果
          communicationPort.postMessage({
            type: 'RESULT',
            id,
            data: result
          });
        } catch (error) {
          communicationPort.postMessage({
            type: 'ERROR',
            id,
            error: error.message
          });
        }
      }
    };
  }
};

// 数据处理函数
async function processData(data) {
  // 模拟复杂计算
  await new Promise(resolve => setTimeout(resolve, 100));
  
  return {
    processed: true,
    timestamp: Date.now(),
    summary: `Processed ${data.length} items`
  };
}

示例2:安全的iframe通信架构

父页面控制器

class IframeCommunicator {
  constructor() {
    this.channels = new Map();
    this.messageHandlers = new Map();
  }
  
  registerIframe(iframeElement, allowedOrigins) {
    const channel = new MessageChannel();
    const iframeId = iframeElement.id;
    
    // 存储通道引用
    this.channels.set(iframeId, {
      port: channel.port1,
      iframe: iframeElement,
      allowedOrigins
    });
    
    // 设置消息监听
    channel.port1.onmessage = (event) => {
      this.handleIncomingMessage(iframeId, event);
    };
    
    // 等待iframe加载完成后发送端口
    iframeElement.addEventListener('load', () => {
      iframeElement.contentWindow.postMessage(
        {
          type: 'CHANNEL_INIT',
          iframeId
        },
        '*',
        [channel.port2]
      );
    });
    
    return {
      send: (type, data) => this.sendToIframe(iframeId, type, data),
      on: (type, handler) => this.registerHandler(iframeId, type, handler)
    };
  }
  
  sendToIframe(iframeId, type, data) {
    const channel = this.channels.get(iframeId);
    if (channel && channel.port) {
      channel.port.postMessage({ type, data });
    }
  }
}

iframe端适配器

class IframeBridge {
  constructor() {
    this.parentPort = null;
    this.handlers = new Map();
    
    window.addEventListener('message', (event) => {
      if (event.data.type === 'CHANNEL_INIT' && event.ports[0]) {
        this.parentPort = event.ports[0];
        
        this.parentPort.onmessage = (messageEvent) => {
          const { type, data } = messageEvent.data;
          this.dispatchMessage(type, data);
        };
        
        // 通知父页面连接就绪
        this.send('READY', { status: 'connected' });
      }
    });
  }
  
  send(type, data) {
    if (this.parentPort) {
      this.parentPort.postMessage({ type, data });
    }
  }
  
  on(type, handler) {
    if (!this.handlers.has(type)) {
      this.handlers.set(type, []);
    }
    this.handlers.get(type).push(handler);
  }
}

四、高级应用与最佳实践

1. 错误处理与重连机制

class RobustMessageChannel {
  constructor(target, options = {}) {
    this.target = target;
    this.maxRetries = options.maxRetries || 3;
    this.reconnectDelay = options.reconnectDelay || 1000;
    this.retryCount = 0;
    
    this.setupChannel();
  }
  
  setupChannel() {
    try {
      this.channel = new MessageChannel();
      this.setupEventListeners();
      
      // 发送端口到目标
      this.target.postMessage('INIT', '*', [this.channel.port2]);
      
      // 设置连接超时
      this.connectionTimeout = setTimeout(() => {
        this.handleDisconnection();
      }, 5000);
      
    } catch (error) {
      this.handleError(error);
    }
  }
  
  handleDisconnection() {
    if (this.retryCount < this.maxRetries) {
      this.retryCount++;
      setTimeout(() => this.setupChannel(), this.reconnectDelay);
    }
  }
}

2. 消息序列化与性能优化

// 使用Transferable对象提升性能
function sendLargeBuffer(port, buffer) {
  // 标记为可转移对象,避免复制
  port.postMessage(
    { type: 'LARGE_BUFFER', buffer },
    [buffer]
  );
}

// 批量消息处理
class MessageBatcher {
  constructor(port, batchSize = 10) {
    this.port = port;
    this.batchSize = batchSize;
    this.queue = [];
    this.flushTimeout = null;
  }
  
  send(type, data) {
    this.queue.push({ type, data, timestamp: Date.now() });
    
    if (this.queue.length >= this.batchSize) {
      this.flush();
    } else if (!this.flushTimeout) {
      this.flushTimeout = setTimeout(() => this.flush(), 50);
    }
  }
  
  flush() {
    if (this.queue.length > 0) {
      this.port.postMessage({
        type: 'BATCH',
        messages: this.queue
      });
      this.queue = [];
    }
    clearTimeout(this.flushTimeout);
    this.flushTimeout = null;
  }
}

3. 类型安全的消息通信

// 使用TypeScript或JSDoc增强类型安全
/**
 * @typedef {Object} MessageProtocol
 * @property {'TASK' | 'RESULT' | 'ERROR'} type
 * @property {string} id
 * @property {any} [data]
 * @property {string} [error]
 */

class TypedMessageChannel {
  /**
   * @param {MessagePort} port 
   */
  constructor(port) {
    this.port = port;
  }
  
  /**
   * @param {'TASK' | 'RESULT' | 'ERROR'} type
   * @param {any} data
   * @returns {Promise<any>}
   */
  send(type, data) {
    return new Promise((resolve, reject) => {
      const messageId = this.generateId();
      
      const handler = (event) => {
        const response = /** @type {MessageProtocol} */ (event.data);
        if (response.id === messageId) {
          this.port.removeEventListener('message', handler);
          if (response.type === 'ERROR') {
            reject(new Error(response.error));
          } else {
            resolve(response.data);
          }
        }
      };
      
      this.port.addEventListener('message', handler);
      this.port.postMessage({ type, id: messageId, data });
    });
  }
}

五、使用注意事项与兼容性

关键注意事项

  1. 端口所有权转移:传递端口时必须在postMessage的第二个参数中声明

    // 正确:声明转移
    target.postMessage('init', '*', [port2]);
    
    // 错误:端口将被冻结
    target.postMessage({ port: port2 }, '*');
    
  2. 内存管理:及时关闭不再使用的端口

    // 通信结束时清理
    port.close();
    channel = null;
    
  3. 数据类型限制:结构化克隆算法不支持函数、DOM节点等

    • 支持:对象、数组、Blob、ArrayBuffer、ImageBitmap等

    • 不支持:函数、Symbol、DOM节点、原型链

  4. 安全考虑

    • 验证消息来源

    • 设置消息超时

    • 实施速率限制

兼容性处理

function createCommunicationChannel(target) {
  // 检测MessageChannel支持
  if (typeof MessageChannel !== 'undefined') {
    const channel = new MessageChannel();
    target.postMessage('init', '*', [channel.port2]);
    return channel.port1;
  } else {
    // 降级方案:使用postMessage + 消息ID
    return new LegacyChannel(target);
  }
}

class LegacyChannel {
  constructor(target) {
    this.target = target;
    this.listeners = new Map();
    window.addEventListener('message', this.handleMessage.bind(this));
  }
  
  postMessage(data) {
    this.target.postMessage({
      _legacyChannel: true,
      data
    }, '*');
  }
}

六、性能对比与选型建议

MessageChannel vs postMessage

选型建议

  • 选择MessageChannel当

    1. 需要高频双向通信

    2. 要求通信隔离和安全性

    3. 传输大量或敏感数据

    4. 需要精准的"请求-响应"模式

  • 使用postMessage当

    1. 简单的单向通知

    2. 广播消息到多个目标

    3. 兼容旧版浏览器

    4. 轻量级通信需求

七、总结

MessageChannel是现代前端架构中不可或缺的通信工具,它解决了跨执行环境通信的关键问题:

核心价值

  1. 性能卓越:专用通道避免全局事件竞争,提升通信效率

  2. 安全可靠:端口隔离机制防止消息泄露和污染

  3. 架构清晰:明确的"端口对"模型简化了复杂通信逻辑

  4. 功能强大:支持可转移对象、双向通信、错误处理等高级特性

适用场景总结

  • ✅ Web Worker与主线程的高频数据交换

  • ✅ 微前端架构中的模块通信

  • ✅ 复杂iframe应用的父子页面交互

  • ✅ 需要严格隔离的插件系统

  • ✅ 实时数据处理管道

最佳实践要点

  1. 始终实现错误处理和重连逻辑

  2. 通信结束后及时清理端口资源

  3. 对于大型数据传输使用Transferable对象

  4. 在生产环境中添加监控和日志记录

  5. 考虑降级方案以保证兼容性

随着Web应用越来越复杂,对执行环境隔离和高效通信的需求日益增长。MessageChannel提供的专属、双向、高性能通信能力,使其成为构建现代化、模块化前端应用的基石技术。掌握MessageChannel不仅能够解决具体的通信问题,更能帮助你设计出更清晰、更可维护的前端架构。

控制台快速查看自己的log,提高开发效率

前言

对于每一个前端程序员来说,console一下看看后端返回了什么值,文件解析出了什么值,组件传了什么值等等是非常常见的操作。但令人遗憾的是历经几家公司,大家的控制出总是充斥着无数的

  • 内部第三方包的日志
  • 内部通用方法(调接口,登录等)的日志
  • 各种微前端初始化子应用导致的一些值找不到,或者vue被二次初始化导致的大量warning
  • 各种异步函数在初始化没有值时没有try catch导致的typeError 或者 undeifned等等日志
  • 甚至还有大量的人为输出日志混杂其中

以上种种导致你的console被淹没在人海,而我们的诉求也非常的简单,就是快速的找到我打印的这个值是什么。本来2025年了这种诉求应该非常简单,但上网一搜都是在用console炫技,搞一堆有的没的,插入图片的都来了。真的有人会没事干在控制台里插图片么?工作量不饱和么?

利用console.log('c%')

使用占位符可以为你的console添加一些自定义样式,使他在控制台中脱颖而出,方便你快速找到。关于这部分用法不再赘述,已经烂大街了。

const fuck = 1111;
console.log('%c🚀---   fuck   ---', 'color: #fff; background: green; padding: 2px 6px;border-radius: 3px;', fuck)

利用插件Turbo Console

每次都要写一长串长长的console非常降智,所以可以将他封装成一个方法在项目中快速调用。但是单独为了看日志这么做也挺傻的,而且控制台一堆乱七八糟东西的公司你封装了也大概只有你用,没必要在项目中加这种。借助现有插件满足自己的诉求即可。

本来想直接让AI帮我完成插件配置,但试了几次都是不行。又搜了几篇文章也是废话和乱七八糟配置一大堆还不能用。下面是我自己的配置

操作流程如下

  1. 拓展商店搜索安装Turbo Console
  2. ctrl + shift + p 打开vscode搜索框,输入:settings.json,选择用户区设置

image.png

  1. 复制粘贴这段代码 简单来讲就是 你打印变量 的前后缀,以及他的样式,将他调整为上一节中console的格式即可。
{
    "turboConsoleLog.logMessageSuffix": "   ---', 'color: #fff; background: green; padding: 2px 6px;border-radius: 3px;",
    "turboConsoleLog.logMessagePrefix": "%c🚀---",
    "turboConsoleLog.quote": "'",
    "turboConsoleLog.delimiterInsideMessage": " "
}
  1. ctrl + s 保存被配置
  2. 回到你的文件
  3. ctrl + shift + p 打开vscode搜索框,输入:Develop:reload window找到重启windows窗口使插件的配置得到更新。

image.png

  1. 双击选中你想要打印的变量ctrl + k 摁两次 或者 shift + alt + l 快速生成console
  2. 最终结果

image.png

  1. 插件的其他快捷键
  • ctrl + alt + l 选中变量之后,使用这个快捷键生成 console.log

  • alt + shift + c 注释所有 console.log

  • alt + shift + u 启用所有 console.log

  • alt + shift + d 删除所有 console.log

利用控制台过滤

虽然一系列方法使得日志在控制台脱颖而出了,但有些控制台的日志量简直是Amazing Unbelievable!!! 所以结合上一步插入的小图标:🚀,直接在控制台过滤,世界立马就安静了。

image.png

当然这有一个弊端,就是当你发现别人的console怎么没输出的时候,一定要记得你开了过滤,要把过滤去掉

必知Node应用性能提升及API test 接口测试

用户注意力无法超过 10s

image.png

提升性能方法

使用 多个进程

image.png

image.png

可以让单线程节点应用充分使用 CPU, 每个 CPU 内核都能并行运行代码

Node 原生 cluster

// cluster.js
const cluster = require('cluster')
const os = require('os')

const WORKERS = Number(process.env.WORKERS || os.cpus().length)

if (cluster.isPrimary) {
  console.log(`[master] pid=${process.pid}, workers=${WORKERS}`)

  for (let i = 0; i < WORKERS; i++) cluster.fork()

  // worker 异常退出自动拉起
  cluster.on('exit', (worker, code, signal) => {
    console.error(`[master] worker ${worker.process.pid} exit code=${code} signal=${signal}, restart...`)
    cluster.fork()
  })
} else {
  require('./app') // 这里引入你的服务入口
}

// app.js
const http = require('http')

const server = http.createServer((req, res) => {
  res.end(`hello from pid=${process.pid}\n`)
})

const PORT = Number(process.env.PORT || 3000)
server.listen(PORT, () => {
  console.log(`[worker] pid=${process.pid} listening on ${PORT}`)
})

// 优雅退出(配合 reload/restart 更稳)
process.on('SIGTERM', () => {
  server.close(() => process.exit(0))
})

启动

node cluster.js
# 或指定进程数
WORKERS=4 node cluster.js

集群

image.png

image.png

image.png

image.png

负载均衡

image.png

最常用:Nginx upstream 负载均衡(推荐)

适合:多台机器、多容器、多实例;稳定、功能全(健康检查、限流、TLS、缓存等)。

upstream api_upstream {
  least_conn;            # 也可用 round_robin(默认) / ip_hash(粘性)
  server 127.0.0.1:3001 max_fails=3 fail_timeout=10s;
  server 127.0.0.1:3002 max_fails=3 fail_timeout=10s;
  server 127.0.0.1:3003 max_fails=3 fail_timeout=10s;
}

server {
  listen 80;

  location / {
    proxy_pass http://api_upstream;

    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_http_version 1.1;
  }
}

水平扩展

image.png

PM2 (包含 集群模块)

Node 单进程吃不满多核,PM2 用 cluster 模式横向扩。

# 启动:按 CPU 核心数拉起多进程
pm2 start app.js -i max --exec-mode cluster --name myapp

# 或指定数量
pm2 start app.js -i 4 --exec-mode cluster --name myapp

worker thread

image.png

简易线程池 worker-pool.js

const { Worker } = require('worker_threads')

class WorkerPool {
  constructor(size, file) {
    this.workers = []
    this.queue = []

    for (let i = 0; i < size; i++) {
      const worker = new Worker(file)
      worker.busy = false

      worker.on('message', (result) => {
        worker.busy = false
        worker.resolve(result)
        this.next()
      })

      this.workers.push(worker)
    }
  }

  run(data) {
    return new Promise((resolve) => {
      this.queue.push({ data, resolve })
      this.next()
    })
  }

  next() {
    const idle = this.workers.find(w => !w.busy)
    if (!idle || this.queue.length === 0) return

    const task = this.queue.shift()
    idle.busy = true
    idle.resolve = task.resolve
    idle.postMessage(task.data)
  }
}

module.exports = WorkerPool

worker.js(支持 postMessage)

const { parentPort } = require('worker_threads')

function heavyCalc(n) {
  let sum = 0
  for (let i = 0; i < n * 1e7; i++) sum += i
  return sum
}

parentPort.on('message', (n) => {
  parentPort.postMessage(heavyCalc(n))
})

使用线程池

const WorkerPool = require('./worker-pool')

const pool = new WorkerPool(4, './worker.js')

async function test() {
  const result = await pool.run(10)
  console.log(result)
}

test()

测试基础

image.png

放点 代码片段 具体 看仓库

const request = require('supertest')

const app = require('../../app')

 

describe('Test GET /launches', () => {

test('It should respond with 200 success', async() => {

await request(app)

.get('/launches')

.expect('Content-Type', /json/)

.expect(200)

})

})

 

describe('Test POST /launches', () => {

const completeLaunchData = {

"mission": "USS Enterprise",

"rocket": "NCC 1701-D",

"target": "Kepler-186 f",

"launchDate": "January 4, 2028"

}

 

const launchDataWithoutLaunchDate = {

"mission": "USS Enterprise",

"rocket": "NCC 1701-D",

"target": "Kepler-186 f",

}

 

const launchInviteData = {

"mission": "USS Enterprise",

"rocket": "NCC 1701-D",

"target": "Kepler-186 f",

"launchDate": "hello"

}

 

test('It should respond with 201 created', async() => {

const response = await request(app)

.post('/launches')

.send(completeLaunchData)

.expect('Content-Type', /json/)

.expect(201)

 

const requestData = new Date(completeLaunchData.launchDate).valueOf()

const responseData = new Date(response.body.launchDate).valueOf()

expect(requestData).toBe(responseData)

})

 

test('It should catch missing required properties', async() => {

const response = await request(app)

.post('/launches')

.send(launchDataWithoutLaunchDate)

.expect('Content-Type', /json/)

.expect(400)

 

expect(response.body).toStrictEqual({

error:'Missing required launch property'

})

})

 

test('It should catch invalid dates', async() => {

const response = await request(app)

.post('/launches')

.send(launchInviteData)

.expect('Content-Type', /json/)

.expect(400)

 

expect(response.body).toStrictEqual({

error:'Invalid launch date'

})

})

})

仓库

github.com/huanhunmao/…

手写简易Vue响应式:基于Proxy + effect的核心实现

Vue的响应式系统是其核心特性之一,从Vue2到Vue3,响应式的实现方案从Object.defineProperty演进为Proxy。相比前者,Proxy能原生支持数组、对象新增属性等场景,且对对象的拦截更全面。本文将从核心原理出发,手把手教你实现一个基于Proxy + effect的简易Vue响应式系统,帮你彻底搞懂响应式的底层逻辑。

一、响应式的核心原理是什么?

响应式的本质是“数据变化驱动视图更新”,其核心逻辑可拆解为三个关键步骤:

  1. 依赖收集:当组件渲染(或effect执行)时,会访问响应式数据,此时记录“数据-依赖(effect)”的映射关系;
  2. 数据拦截:通过Proxy拦截响应式数据的读取(get)和修改(set)操作——读取时触发依赖收集,修改时触发依赖更新;
  3. 依赖触发:当响应式数据被修改时,找到之前收集的所有依赖(effect),并重新执行这些依赖,从而实现视图更新或其他副作用触发。

其中,Proxy负责“数据拦截”,effect负责封装“依赖(副作用函数)”,再配合一个“依赖映射表”完成整个响应式闭环。

二、核心模块拆解与实现

我们将分三步实现简易响应式系统:先实现effect模块封装副作用,再实现reactive模块基于Proxy拦截数据,最后通过依赖映射表关联两者,完成依赖收集与触发。

1. 第一步:实现effect——副作用函数封装

effect的作用是包裹需要响应式触发的副作用函数(比如组件渲染函数、watch回调等)。当effect执行时,会主动触发响应式数据的get操作,进而触发依赖收集;当数据变化时,effect会被重新执行。

核心逻辑:

  • 定义一个全局变量(activeEffect),用于标记当前正在执行的effect;
  • effect函数接收一个副作用函数(fn),执行fn前将其赋值给activeEffect,执行后清空activeEffect(避免非响应式数据访问时误收集依赖)。

代码实现:

// 全局变量:标记当前活跃的effect(正在执行的副作用函数)
let activeEffect = null;

/**
 * 副作用函数封装
 * @param {Function} fn - 需要响应式触发的副作用函数
 */
function effect(fn) {
  // 定义一个包装函数,便于后续扩展(如错误处理、调度执行等)
  const effectFn = () => {
    // 执行副作用函数前,先标记当前活跃的effect
    activeEffect = effectFn;
    // 执行副作用函数(此时会访问响应式数据,触发get拦截,进而收集依赖)
    fn();
    // 执行完成后,清空标记(避免后续非响应式数据访问时误收集)
    activeEffect = null;
  };

  // 立即执行一次副作用函数,触发初始的依赖收集
  effectFn();
}

2. 第二步:实现依赖映射表——track与trigger

我们需要一个数据结构来存储“数据-属性-effect”的映射关系,这里采用WeakMap(数据)→ Map(属性)→ Set(effect)的结构:

  • WeakMap:key为响应式对象(target),value为Map(属性映射表),弱引用特性可避免内存泄漏;
  • Map:key为对象的属性名(key),value为Set(存储该属性对应的所有effect);
  • Set:存储effect,保证effect不重复(避免多次执行同一副作用)。

基于这个结构,实现两个核心函数:

  • track:在响应式数据被读取时调用,收集依赖(将activeEffect存入映射表);
  • trigger:在响应式数据被修改时调用,触发依赖(从映射表中取出effect并执行)。

代码实现:

// 依赖映射表:WeakMap(target) → Map(key) → Set(effect)
const targetMap = new WeakMap();

/**
 * 收集依赖(响应式数据读取时触发)
 * @param {Object} target - 响应式对象
 * @param {string} key - 被读取的属性名
 */
function track(target, key) {
  // 1. 若当前无活跃的effect,无需收集依赖,直接返回
  if (!activeEffect) return;

  // 2. 从targetMap中获取当前对象的属性映射表(Map)
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    // 若不存在,创建新的Map并存入targetMap
    depsMap = new Map();
    targetMap.set(target, depsMap);
  }

  // 3. 从depsMap中获取当前属性的effect集合(Set)
  let deps = depsMap.get(key);
  if (!deps) {
    // 若不存在,创建新的Set并存入depsMap
    deps = new Set();
    depsMap.set(key, deps);
  }

  // 4. 将当前活跃的effect存入Set(保证不重复)
  deps.add(activeEffect);
}

/**
 * 触发依赖(响应式数据修改时触发)
 * @param {Object} target - 响应式对象
 * @param {string} key - 被修改的属性名
 */
function trigger(target, key) {
  // 1. 从targetMap中获取当前对象的属性映射表
  const depsMap = targetMap.get(target);
  if (!depsMap) return; // 若没有收集过依赖,直接返回

  // 2. 从depsMap中获取当前属性的effect集合
  const deps = depsMap.get(key);
  if (deps) {
    // 3. 遍历effect集合,执行每个effect(触发副作用更新)
    deps.forEach(effect => effect());
  }
}

3. 第三步:实现reactive——基于Proxy的响应式数据拦截

reactive函数的作用是将普通对象转为响应式对象,核心是通过Proxy拦截对象的get(读取)和set(修改)操作:

  • get拦截:当读取响应式对象的属性时,调用track函数收集依赖;
  • set拦截:当修改响应式对象的属性时,先更新属性值,再调用trigger函数触发依赖。

代码实现:

/**
 * 将普通对象转为响应式对象(基于Proxy)
 * @param {Object} target - 普通对象
 * @returns {Proxy} 响应式对象
 */
function reactive(target) {
  return new Proxy(target, {
    // 拦截属性读取操作
    get(target, key) {
      // 1. 读取原始属性值
      const value = Reflect.get(target, key);
      // 2. 收集依赖(关联target、key和当前activeEffect)
      track(target, key);
      // 3. 返回属性值(若value是对象,可递归转为响应式,这里简化实现)
      return value;
    },

    // 拦截属性修改操作
    set(target, key, value) {
      // 1. 修改原始属性值
      const result = Reflect.set(target, key, value);
      // 2. 触发依赖(执行该属性关联的所有effect)
      trigger(target, key);
      // 3. 返回修改结果(符合Proxy规范)
      return result;
    }
  });
}

这里使用Reflect而非直接操作target,是为了保证操作的规范性(比如Reflect.set会返回布尔值表示修改成功,而直接赋值不会),同时与Proxy的拦截行为更匹配。

三、完整测试:验证响应式效果

我们已经实现了effect、track、trigger、reactive四个核心模块,现在编写测试代码验证响应式是否生效:

// 1. 创建普通对象并转为响应式对象
const user = reactive({ name: "张三", age: 20 });

// 2. 定义副作用函数(模拟组件渲染:依赖user.name和user.age)
effect(() => {
  console.log(`姓名:${user.name},年龄:${user.age}`);
});

// 3. 修改响应式数据,观察副作用是否触发
user.name = "李四"; // 输出:姓名:李四,年龄:20(触发effect重新执行)
user.age = 21;      // 输出:姓名:李四,年龄:21(再次触发effect)
user.gender = "男"; // 新增属性(Proxy天然支持,若有依赖该属性的effect也会触发)

运行结果:

  • effect首次执行时,输出“姓名:张三,年龄:20”(初始渲染);
  • 修改user.name时,触发set拦截→trigger→effect重新执行,输出更新后的内容;
  • 修改user.age时,同样触发effect更新;
  • 新增user.gender时,若后续有effect依赖该属性,修改时也会触发更新(本测试中无依赖,故无输出)。

四、核心细节补充与简化点说明

上面的实现是简化版响应式,Vue3的真实响应式系统更复杂,这里补充几个关键细节和简化点:

1. 简化点:未处理嵌套对象

当前reactive函数仅对顶层对象进行Proxy拦截,若对象属性是嵌套对象(如user = { info: { age: 20 } }),修改user.info.age不会触发响应式。解决方法是在get拦截时,对返回的value进行判断,若为对象则递归调用reactive:

// 优化reactive的get拦截
get(target, key) {
  const value = Reflect.get(target, key);
  track(target, key);
  // 递归处理嵌套对象
  return typeof value === 'object' && value !== null ? reactive(value) : value;
}

2. 简化点:未处理数组

Proxy天然支持数组拦截,比如修改数组的push、splice、索引等操作。只需在set拦截时,对数组的特殊操作(如push会新增索引)进行处理,确保trigger能正确触发。当前简化实现已支持数组的索引修改,比如:

const list = reactive([1, 2, 3]);
effect(() => {
  console.log("数组:", list.join(','));
});
list[0] = 10; // 输出:数组:10,2,3(触发effect)
list.push(4); // 输出:数组:10,2,3,4(触发effect)

3. 真实Vue3的扩展:调度执行、computed、watch等

我们的实现仅覆盖了核心响应式逻辑,Vue3还在此基础上扩展了:

  • 调度执行:effect支持传入scheduler选项,实现副作用的延迟执行、防抖、节流等;
  • computed:基于effect实现缓存机制,只有依赖变化时才重新计算;
  • watch:监听响应式数据变化,触发回调函数(支持立即执行、深度监听等);
  • Ref:处理基本类型的响应式(通过封装对象实现,核心还是Proxy)。

五、总结

本文通过“effect封装副作用 → track/trigger管理依赖 → reactive基于Proxy拦截数据”的步骤,实现了一个简易的Vue响应式系统。核心逻辑可概括为:

effect执行时标记活跃状态,访问响应式数据触发get拦截,通过track收集“数据-属性-effect”依赖;修改数据触发set拦截,通过trigger找到对应依赖并重新执行effect,最终实现响应式更新。

理解这个核心逻辑后,再去学习Vue3的computed、watch等API的实现原理,就会变得非常轻松。建议你动手敲一遍代码,尝试修改和扩展(比如添加嵌套对象支持、调度执行),加深对响应式原理的理解。

❌