普通视图

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

网页版时钟

作者 rocky191
2026年1月15日 20:45

之前看到类似的时钟工具,ai coding 一个类似的!浏览器全屏显示后,当成屏保,也不错。

image.png

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>数字时钟</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: 'Arial', sans-serif;
            background-color: #ffffff;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            min-height: 100vh;
            padding: 20px;
            color: #333;
        }

        .container {
            text-align: center;
            max-width: 800px;
        }

        .date {
            font-size: 2.5rem;
            color: #666;
            margin-bottom: 40px;
            font-weight: 300;
        }

        .clock-container {
            display: flex;
            justify-content: center;
            align-items: center;
            gap: 8px;
            margin-bottom: 60px;
        }

        .time-segment {
            position: relative;
            width: 80px;
            height: 120px;
            background-color: #e0e0e0;
            border-radius: 8px;
            overflow: hidden;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
        }

        .time-digit {
            position: absolute;
            width: 100%;
            height: 100%;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 4rem;
            font-weight: bold;
            color: #333;
        }

        .time-separator {
            font-size: 3rem;
            color: #333;
            font-weight: bold;
        }

        .ampm {
            font-size: 2.5rem;
            color: #333;
            margin-left: 15px;
            font-weight: bold;
        }

        .quote {
            font-size: 1.2rem;
            color: #666;
            line-height: 1.6;
            margin-bottom: 20px;
            font-style: italic;
        }

        .author {
            font-size: 1rem;
            color: #888;
            text-align: right;
        }

        @media (max-width: 768px) {
            .date {
                font-size: 2rem;
            }

            .time-segment {
                width: 60px;
                height: 90px;
            }

            .time-digit {
                font-size: 3rem;
            }

            .time-separator {
                font-size: 2.5rem;
            }

            .ampm {
                font-size: 2rem;
            }

            .quote {
                font-size: 1rem;
            }
        }

        @media (max-width: 480px) {
            .date {
                font-size: 1.5rem;
            }

            .time-segment {
                width: 45px;
                height: 70px;
            }

            .time-digit {
                font-size: 2.2rem;
            }

            .time-separator {
                font-size: 2rem;
            }

            .ampm {
                font-size: 1.5rem;
            }

            .quote {
                font-size: 0.9rem;
            }
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="date" id="date">星期二, 九月 23 2025</div>
        
        <div class="clock-container">
            <div class="time-segment">
                <div class="time-digit" id="hour1">1</div>
            </div>
            <div class="time-segment">
                <div class="time-digit" id="hour2">1</div>
            </div>
            <div class="time-separator">:</div>
            <div class="time-segment">
                <div class="time-digit" id="minute1">3</div>
            </div>
            <div class="time-segment">
                <div class="time-digit" id="minute2">0</div>
            </div>
            <div class="time-separator">:</div>
            <div class="time-segment">
                <div class="time-digit" id="second1">3</div>
            </div>
            <div class="time-segment">
                <div class="time-digit" id="second2">8</div>
            </div>
            <div class="ampm" id="ampm">PM</div>
        </div>
        
        <div class="quote">
            "When I get a little money I buy books; and if any is left I buy food and clothes."
        </div>
        <div class="author">Desiderius Erasmus</div>
    </div>

    <script>
        // 星期和月份的中文映射
        const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
        const months = ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'];
        
        function updateClock() {
            const now = new Date();
            
            // 获取中文日期
            const year = now.getFullYear();
            const month = now.getMonth();
            const day = now.getDate();
            const weekday = now.getDay();
            
            // 设置日期(使用中文格式)
            const dateElement = document.getElementById('date');
            dateElement.textContent = `${weekdays[weekday]}, ${months[month]} ${day} ${year}`;
            
            // Get current time
            let hours = now.getHours();
            const minutes = now.getMinutes();
            const seconds = now.getSeconds();
            const ampm = hours >= 12 ? 'PM' : 'AM';
            
            // Convert to 12-hour format
            hours = hours % 12;
            hours = hours ? hours : 12; // the hour '0' should be '12'
            
            // Format time with leading zeros
            const formattedHours = hours.toString().padStart(2, '0');
            const formattedMinutes = minutes.toString().padStart(2, '0');
            const formattedSeconds = seconds.toString().padStart(2, '0');
            
            // Update the clock display
            document.getElementById('hour1').textContent = formattedHours[0];
            document.getElementById('hour2').textContent = formattedHours[1];
            document.getElementById('minute1').textContent = formattedMinutes[0];
            document.getElementById('minute2').textContent = formattedMinutes[1];
            document.getElementById('second1').textContent = formattedSeconds[0];
            document.getElementById('second2').textContent = formattedSeconds[1];
            document.getElementById('ampm').textContent = ampm;
        }
        
        // Update clock immediately and then every second
        updateClock();
        setInterval(updateClock, 1000);
    </script>
</body>
</html>

最大化网格图中正方形空洞的面积

2026年1月12日 12:41

方法一:排序

思路与算法

题目要求通过移除部分横线段和竖线段,使剩余网格图中的正方形空洞面积最大。可以发现,正方形空洞的边长取决于移除的横向和纵向最大连续线段数目。因此,具体做法如下:

  1. 首先对 $\textit{hBars}$ 和 $\textit{vBars}$ 从小到大进行排序,方便后续计算连续线段数目。
  2. 分别遍历排序后的数组 $\textit{hBars}$ 和 $\textit{vBars}$,统计横向最大连续线段数目 $\textit{hmax}$ 和纵向最大连续线段数目 $\textit{vmax}$。
  3. 计算最大正方形边长 $\textit{side}$ 为 $\min(hmax, vmax) + 1$,返回面积即为边长的平方 $\textit{side}^2$。

代码

###C++

class Solution {
public:
    int maximizeSquareHoleArea(int n, int m, vector<int>& hBars, vector<int>& vBars) {
        sort(hBars.begin(), hBars.end());
        sort(vBars.begin(), vBars.end());
        int hmax = 1, vmax = 1;
        int hcur = 1, vcur = 1;
        for (int i = 1; i < hBars.size(); i++) {
            if (hBars[i] == hBars[i - 1] + 1) {
                hcur++;
            } else {
                hcur = 1;
            }
            hmax = max(hmax, hcur);
        }
        for (int i = 1; i < vBars.size(); i++) {
            if (vBars[i] == vBars[i - 1] + 1) {
                vcur++;
            } else {
                vcur = 1;
            }
            vmax = max(vmax, vcur);
        }
        int side = min(hmax, vmax) + 1;
        return side * side;
    }
};

###Go

func maximizeSquareHoleArea(n int, m int, hBars []int, vBars []int) int {
    sort.Ints(hBars)
    sort.Ints(vBars)
    hmax, vmax := 1, 1
    hcur, vcur := 1, 1
    for i := 1; i < len(hBars); i++ {
        if hBars[i] == hBars[i - 1] + 1 {
            hcur++
        } else {
            hcur = 1
        }
        hmax = max(hmax, hcur)
    }
    for i := 1; i < len(vBars); i++ {
        if vBars[i] == vBars[i - 1] + 1 {
            vcur++
        } else {
            vcur = 1
        }
        vmax = max(vmax, vcur)
    }
    side := min(hmax, vmax) + 1
    return side * side
}

###Python

class Solution:
    def maximizeSquareHoleArea(self, n: int, m: int, hBars: List[int], vBars: List[int]) -> int:
        hBars.sort()
        vBars.sort()
        hmax, vmax = 1, 1
        hcur, vcur = 1, 1
        for i in range(1, len(hBars)):
            if hBars[i] == hBars[i - 1] + 1:
                hcur += 1
            else:
                hcur = 1
            hmax = max(hmax, hcur)
        for i in range(1, len(vBars)):
            if vBars[i] == vBars[i - 1] + 1:
                vcur += 1
            else:
                vcur = 1
            vmax = max(vmax, vcur)
        side = min(hmax, vmax) + 1
        return side * side

###Java

class Solution {
    public int maximizeSquareHoleArea(int n, int m, int[] hBars, int[] vBars) {
        Arrays.sort(hBars);
        Arrays.sort(vBars);
        int hmax = 1, vmax = 1;
        int hcur = 1, vcur = 1;
        for (int i = 1; i < hBars.length; i++) {
            if (hBars[i] == hBars[i - 1] + 1) {
                hcur++;
            } else {
                hcur = 1;
            }
            hmax = Math.max(hmax, hcur);
        }
        for (int i = 1; i < vBars.length; i++) {
            if (vBars[i] == vBars[i - 1] + 1) {
                vcur++;
            } else {
                vcur = 1;
            }
            vmax = Math.max(vmax, vcur);
        }
        int side = Math.min(hmax, vmax) + 1;
        return side * side;
    }
}

###TypeScript

function maximizeSquareHoleArea(n: number, m: number, hBars: number[], vBars: number[]): number {
    hBars.sort((a, b) => a - b);
    vBars.sort((a, b) => a - b);
    let hmax = 1, vmax = 1;
    let hcur = 1, vcur = 1;
    for (let i = 1; i < hBars.length; i++) {
        if (hBars[i] === hBars[i - 1] + 1) {
            hcur++;
        } else {
            hcur = 1;
        }
        hmax = Math.max(hmax, hcur);
    }
    for (let i = 1; i < vBars.length; i++) {
        if (vBars[i] === vBars[i - 1] + 1) {
            vcur++;
        } else {
            vcur = 1;
        }
        vmax = Math.max(vmax, vcur);
    }
    const side = Math.min(hmax, vmax) + 1;
    return side * side;
}

###JavaScript

function maximizeSquareHoleArea(n, m, hBars, vBars) {
    hBars.sort((a, b) => a - b);
    vBars.sort((a, b) => a - b);
    let hmax = 1, vmax = 1;
    let hcur = 1, vcur = 1;
    for (let i = 1; i < hBars.length; i++) {
        if (hBars[i] === hBars[i - 1] + 1) {
            hcur++;
        } else {
            hcur = 1;
        }
        hmax = Math.max(hmax, hcur);
    }
    for (let i = 1; i < vBars.length; i++) {
        if (vBars[i] === vBars[i - 1] + 1) {
            vcur++;
        } else {
            vcur = 1;
        }
        vmax = Math.max(vmax, vcur);
    }
    const side = Math.min(hmax, vmax) + 1;
    return side * side;
}

###C#

public class Solution {
    public int MaximizeSquareHoleArea(int n, int m, int[] hBars, int[] vBars) {
        Array.Sort(hBars);
        Array.Sort(vBars);
        int hmax = 1, vmax = 1;
        int hcur = 1, vcur = 1;
        for (int i = 1; i < hBars.Length; i++) {
            if (hBars[i] == hBars[i - 1] + 1) {
                hcur++;
            } else {
                hcur = 1;
            }
            hmax = Math.Max(hmax, hcur);
        }
        for (int i = 1; i < vBars.Length; i++) {
            if (vBars[i] == vBars[i - 1] + 1) {
                vcur++;
            } else {
                vcur = 1;
            }
            vmax = Math.Max(vmax, vcur);
        }
        int side = Math.Min(hmax, vmax) + 1;
        return side * side;
    }
}

###C

int compare(const void* a, const void* b) {
    return *(int*)a - *(int*)b;
}

int maximizeSquareHoleArea(int n, int m, int* hBars, int hBarsSize, int* vBars, int vBarsSize) {
    qsort(hBars, hBarsSize, sizeof(int), compare);
    qsort(vBars, vBarsSize, sizeof(int), compare);
    int hmax = 1, vmax = 1;
    int hcur = 1, vcur = 1;
    for (int i = 1; i < hBarsSize; i++) {
        if (hBars[i] == hBars[i - 1] + 1) {
            hcur++;
        } else {
            hcur = 1;
        }
        hmax = fmax(hmax, hcur);
    }
    for (int i = 1; i < vBarsSize; i++) {
        if (vBars[i] == vBars[i - 1] + 1) {
            vcur++;
        } else {
            vcur = 1;
        }
        vmax = fmax(vmax, vcur);
    }
    int side = fmin(hmax, vmax) + 1;
    return side * side;
}

###Rust

use std::cmp;

impl Solution {
    pub fn maximize_square_hole_area(n: i32, m: i32, mut h_bars: Vec<i32>, mut v_bars: Vec<i32>) -> i32 {
        h_bars.sort_unstable();
        v_bars.sort_unstable();
        let mut hmax = 1;
        let mut vmax = 1;
        let mut hcur = 1;
        let mut vcur = 1;
        for i in 1..h_bars.len() {
            if h_bars[i] == h_bars[i - 1] + 1 {
                hcur += 1;
            } else {
                hcur = 1;
            }
            hmax = cmp::max(hmax, hcur);
        }
        for i in 1..v_bars.len() {
            if v_bars[i] == v_bars[i - 1] + 1 {
                vcur += 1;
            } else {
                vcur = 1;
            }
            vmax = cmp::max(vmax, vcur);
        }
        let side = cmp::min(hmax, vmax) + 1;
        side * side
    }
}

复杂度分析

  • 时间复杂度:$O(h \log h + v \log v)$,其中 $h$ 和 $v$ 分别为数组 $\textit{hBars}$ 和 $\textit{vBars}$ 的长度。对 $\textit{hBars}$ 和 $\textit{vBars}$ 排序分别需要 $O(h \log h)$ 和 $O(v \log v)$。

  • 空间复杂度:$O(\log h + \log v)$。对 $\textit{hBars}$ 和 $\textit{vBars}$ 排序分别需要 $O(\log h)$ 和 $O(\log v)$ 的栈空间。

Node.js 存在多个严重安全漏洞!官方建议尽快升级🚀🚀🚀

2026年1月15日 19:07

前言

Node.js 官方在 2026 年 1 月 13 日更新修复了多个严重安全漏洞,涉及缓冲区泄露、权限绕过、DoS 攻击等,影响所有活跃版本,建议所有用户尽快升级!

往期精彩推荐

正文

本次安全发布主要针对 Node.js 当前所有活跃版本(20.x、22.x、24.x、25.x)发布了补丁,累计修复了 3 个 高危4 个 中危1 个 低危 严重性的漏洞,

同时更新了核心依赖 c-ares 和 undici。

主要高危漏洞:

  1. CVE-2025-55131(High)

Timeout 导致的竞态条件,使得 Buffer.alloc / Uint8Array 可能分配到未清零的内存,存在敏感信息泄露风险(密钥、token 等)。需要特定时机或攻击者控制超时行为才能利用,但危害极大。

  1. CVE-2025-55130(High)

使用精心构造的相对路径符号链接,可绕过 --allow-fs-read / --allow-fs-write 权限限制,实现任意文件读写。严重破坏了 Permission Model 的隔离能力。

  1. CVE-2025-59465(High)

HTTP/2 服务器在收到畸形 HEADERS 帧时会直接抛出未处理的 TLSSocket 错误,导致进程崩溃(远程 DoS)。未加 error 监听的 HTTPS 服务尤其危险。

中危漏洞:

  • CVE-2026-21636(Medium):

权限模型下 Unix Domain Socket 未受 --allow-net 限制,可连接任意本地 socket,存在提权风险(仅影响 v25)

  • CVE-2025-59466(Medium):

async_hooks 场景下栈溢出错误无法被捕获,直接终止进程(DoS)

  • CVE-2026-21637(Medium):

TLS PSK/ALPN 回调中同步异常会绕过错误处理,导致崩溃或 fd 泄漏

低危漏洞

  • CVE-2025-55132(Low):

fs.futimes() 可绕过只读权限修改文件时间戳(痕迹清理)。

受影响版本:所有活跃分支 20.x、22.x、24.x、25.x(EOL 版本同样受影响,但官方不再修复)
修复版本:2025 年 12 月 15 日之后发布的最新 20.x / 22.x / 24.x / 25.x 版本

最后

强烈建议生产环境尽快升级到最新版本,同时关注后续的 async_hooks DoS 缓解方案。

今天的分享就这些了,感谢大家的阅读,如果文章中存在错误的地方欢迎指正!

往期精彩推荐

Vue 自定义指令生命周期钩子完全指南

作者 北辰alk
2026年1月15日 18:52

Vue 自定义指令生命周期钩子完全指南

Vue 自定义指令提供了强大的生命周期钩子,让你可以精准控制指令在 DOM 元素上的行为。本文将深入解析所有钩子函数,并提供丰富的实用示例!

一、指令生命周期概览

1.1 生命周期钩子总览

const myDirective = {
  // 1. 绑定前(Vue 3 新增)
  beforeMount() {},
  
  // 2. 元素挂载时
  mounted() {},
  
  // 3. 更新前(Vue 2: bind → Vue 3: beforeUpdate)
  beforeUpdate() {},
  
  // 4. 更新后
  updated() {},
  
  // 5. 卸载前(Vue 2: unbind → Vue 3: beforeUnmount)
  beforeUnmount() {},
  
  // 6. 卸载后(Vue 2: unbind → Vue 3: unmounted)
  unmounted() {},
  
  // 7. Vue 2 特有
  bind() {},      // Vue 3 中被 beforeMount + mounted 替代
  inserted() {},  // Vue 3 中被 mounted 替代
  componentUpdated() {}, // Vue 3 中被 updated 替代
  unbind() {}     // Vue 3 中被 beforeUnmount + unmounted 替代
}

1.2 Vue 2 vs Vue 3 对比

// Vue 2 指令生命周期
const vue2Directive = {
  bind(el, binding, vnode, oldVnode) {
    // 只调用一次,指令第一次绑定到元素时调用
  },
  inserted(el, binding, vnode, oldVnode) {
    // 被绑定元素插入父节点时调用
  },
  update(el, binding, vnode, oldVnode) {
    // 所在组件的 VNode 更新时调用
  },
  componentUpdated(el, binding, vnode, oldVnode) {
    // 指令所在组件的 VNode 及其子 VNode 全部更新后调用
  },
  unbind(el, binding, vnode, oldVnode) {
    // 只调用一次,指令与元素解绑时调用
  }
}

// Vue 3 指令生命周期(组合式API风格)
const vue3Directive = {
  beforeMount(el, binding, vnode, prevVnode) {
    // 元素挂载前调用
  },
  mounted(el, binding, vnode, prevVnode) {
    // 元素挂载后调用
  },
  beforeUpdate(el, binding, vnode, prevVnode) {
    // 元素更新前调用
  },
  updated(el, binding, vnode, prevVnode) {
    // 元素更新后调用
  },
  beforeUnmount(el, binding, vnode, prevVnode) {
    // 元素卸载前调用
  },
  unmounted(el, binding, vnode, prevVnode) {
    // 元素卸载后调用
  }
}

二、指令钩子函数详解

2.1 钩子参数解析

const directive = {
  mounted(el, binding, vnode, prevVnode) {
    // 参数详解:
    
    // 1. el - 指令绑定的元素
    console.log('元素:', el)  // DOM 元素
    
    // 2. binding - 包含指令信息的对象
    console.log('binding 对象:', {
      // 指令的值(v-my-directive="value" 中的 value)
      value: binding.value,
      
      // 旧值(仅在 beforeUpdate 和 updated 中可用)
      oldValue: binding.oldValue,
      
      // 参数(v-my-directive:arg 中的 arg)
      arg: binding.arg,
      
      // 修饰符对象(v-my-directive.modifier 中的 modifier)
      modifiers: binding.modifiers,
      
      // 指令的实例
      instance: binding.instance,
      
      // 指令的定义对象
      dir: binding.dir
    })
    
    // 3. vnode - 绑定元素的虚拟节点
    console.log('虚拟节点:', {
      type: vnode.type,
      props: vnode.props,
      children: vnode.children,
      el: vnode.el,  // 对应的 DOM 元素
      component: vnode.component,  // 组件实例
      dirs: vnode.dirs  // 指令数组
    })
    
    // 4. prevVnode - 先前的虚拟节点(仅在更新钩子中可用)
    console.log('先前虚拟节点:', prevVnode)
  }
}

2.2 钩子执行时机详解

<template>
  <div>
    <!-- 指令生命周期演示 -->
    <div 
      v-lifecycle-demo="counter" 
      v-if="showElement"
      :class="{ active: isActive }"
    >
      指令生命周期演示
    </div>
    
    <button @click="increment">增加: {{ counter }}</button>
    <button @click="toggleElement">切换显示</button>
    <button @click="toggleClass">切换类名</button>
  </div>
</template>

<script>
// 生命周期演示指令
const lifecycleDemo = {
  beforeMount(el, binding) {
    console.log('1. beforeMount - 绑定前', {
      value: binding.value,
      elementExists: !!el.parentNode
    })
  },
  
  mounted(el, binding, vnode) {
    console.log('2. mounted - 挂载完成', {
      value: binding.value,
      elementInDOM: document.body.contains(el),
      parent: el.parentNode?.tagName
    })
    
    // 添加初始样式
    el.style.transition = 'all 0.3s ease'
  },
  
  beforeUpdate(el, binding) {
    console.log('3. beforeUpdate - 更新前', {
      oldValue: binding.oldValue,
      newValue: binding.value,
      willUpdate: binding.value !== binding.oldValue
    })
  },
  
  updated(el, binding) {
    console.log('4. updated - 更新完成', {
      oldValue: binding.oldValue,
      newValue: binding.value,
      elementText: el.textContent
    })
    
    // 根据值变化添加动画
    if (binding.value > binding.oldValue) {
      el.style.transform = 'scale(1.1)'
      setTimeout(() => {
        el.style.transform = 'scale(1)'
      }, 300)
    }
  },
  
  beforeUnmount(el, binding) {
    console.log('5. beforeUnmount - 卸载前', {
      value: binding.value,
      elementInDOM: document.body.contains(el)
    })
    
    // 添加淡出动画
    el.style.opacity = '0.5'
  },
  
  unmounted(el, binding) {
    console.log('6. unmounted - 卸载完成', {
      value: binding.value,
      elementInDOM: false,  // 此时元素已从DOM移除
      elementReference: el  // el 仍然可以访问,但已不在DOM中
    })
  }
}

export default {
  directives: {
    'lifecycle-demo': lifecycleDemo
  },
  data() {
    return {
      counter: 0,
      showElement: true,
      isActive: false
    }
  },
  methods: {
    increment() {
      this.counter++
    },
    toggleElement() {
      this.showElement = !this.showElement
    },
    toggleClass() {
      this.isActive = !this.isActive
    }
  },
  mounted() {
    console.log('组件 mounted - 开始观察指令生命周期')
  }
}
</script>

三、实用指令示例

3.1 焦点管理指令

// 自动聚焦指令
const vFocus = {
  mounted(el, binding) {
    const { value = true, arg = 'auto', modifiers } = binding
    
    if (value) {
      // 立即聚焦
      if (arg === 'immediate') {
        el.focus()
      }
      // 延迟聚焦
      else if (arg === 'delay') {
        setTimeout(() => {
          el.focus()
        }, binding.value.delay || 100)
      }
      // 条件聚焦
      else if (arg === 'conditional') {
        if (binding.value.condition) {
          el.focus()
        }
      }
      // 自动聚焦(默认)
      else {
        // 对于输入框,自动聚焦
        if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
          el.focus()
        }
      }
      
      // 处理修饰符
      if (modifiers.select) {
        el.select()
      }
      
      if (modifiers.end) {
        el.setSelectionRange(el.value.length, el.value.length)
      }
    }
  },
  
  updated(el, binding) {
    // 值变化时重新聚焦
    if (binding.value !== binding.oldValue && binding.value) {
      el.focus()
      
      if (binding.modifiers.select) {
        el.select()
      }
    }
  },
  
  beforeUnmount(el) {
    // 卸载前移除焦点
    el.blur()
  }
}

// 用法示例
// <input v-focus>                    // 自动聚焦
// <input v-focus.immediate>         // 立即聚焦
// <input v-focus:delay="{delay: 500}"> // 延迟500ms聚焦
// <input v-focus:conditional="{condition: shouldFocus}"> // 条件聚焦
// <input v-focus.select>            // 聚焦并选中文本
// <input v-focus.end>               // 聚焦到末尾

3.2 点击外部指令

// 点击外部关闭指令
const vClickOutside = {
  beforeMount(el, binding) {
    // 创建事件处理函数
    el._clickOutsideHandler = function(event) {
      // 检查点击是否在元素外部
      if (!(el === event.target || el.contains(event.target))) {
        // 调用绑定函数
        binding.value(event)
      }
    }
    
    // 添加事件监听
    document.addEventListener('click', el._clickOutsideHandler)
    
    // 可选:添加其他事件类型
    if (binding.modifiers.mousedown) {
      document.addEventListener('mousedown', el._clickOutsideHandler)
    }
    
    if (binding.modifiers.touchstart) {
      document.addEventListener('touchstart', el._clickOutsideHandler)
    }
  },
  
  updated(el, binding) {
    // 更新时检查值是否变化
    if (binding.value !== binding.oldValue) {
      // 可以在这里更新处理逻辑
      console.log('点击外部指令值已更新')
    }
  },
  
  unmounted(el, binding) {
    // 移除事件监听
    document.removeEventListener('click', el._clickOutsideHandler)
    
    if (binding.modifiers.mousedown) {
      document.removeEventListener('mousedown', el._clickOutsideHandler)
    }
    
    if (binding.modifiers.touchstart) {
      document.removeEventListener('touchstart', el._clickOutsideHandler)
    }
    
    // 清理引用
    delete el._clickOutsideHandler
  }
}

// 高级版本:支持配置和动态启用/禁用
const vClickOutsideAdvanced = {
  mounted(el, binding) {
    const { value, modifiers, arg } = binding
    
    // 默认配置
    const defaultConfig = {
      handler: value,
      events: ['click'],
      enabled: true,
      capture: false,
      immediate: false
    }
    
    // 合并配置
    const config = typeof value === 'function' 
      ? { ...defaultConfig, handler: value }
      : { ...defaultConfig, ...value }
    
    // 处理修饰符
    if (modifiers.mousedown) config.events.push('mousedown')
    if (modifiers.touchstart) config.events.push('touchstart')
    if (modifiers.capture) config.capture = true
    if (modifiers.immediate) config.immediate = true
    
    // 处理参数
    if (arg === 'except') {
      // 排除某些元素
      config.except = binding.value?.except || []
    }
    
    // 存储配置
    el._clickOutsideConfig = config
    
    // 事件处理函数
    el._clickOutsideHandler = (event) => {
      if (!config.enabled) return
      
      // 检查点击是否在排除列表中
      if (config.except) {
        const clickedInsideExcept = config.except.some(selector => {
          const elements = document.querySelectorAll(selector)
          return Array.from(elements).some(element => 
            element.contains(event.target)
          )
        })
        
        if (clickedInsideExcept) return
      }
      
      // 检查点击是否在元素外部
      if (!(el === event.target || el.contains(event.target))) {
        config.handler(event, el)
      }
    }
    
    // 添加事件监听
    config.events.forEach(eventName => {
      document.addEventListener(
        eventName, 
        el._clickOutsideHandler, 
        config.capture
      )
    })
    
    // 立即执行一次检查(如果配置了)
    if (config.immediate) {
      // 模拟外部点击
      setTimeout(() => {
        const fakeEvent = new MouseEvent('click')
        el._clickOutsideHandler(fakeEvent)
      })
    }
  },
  
  updated(el, binding) {
    const oldConfig = el._clickOutsideConfig
    const newValue = binding.value
    
    // 更新配置
    if (typeof newValue === 'function') {
      el._clickOutsideConfig.handler = newValue
    } else if (typeof newValue === 'object') {
      Object.assign(el._clickOutsideConfig, newValue)
    }
    
    // 如果启停状态变化,可能需要更新事件监听
    if (oldConfig?.enabled !== el._clickOutsideConfig?.enabled) {
      console.log('点击外部指令启停状态变化')
    }
  },
  
  beforeUnmount(el) {
    const config = el._clickOutsideConfig
    
    if (config && el._clickOutsideHandler) {
      // 移除所有事件监听
      config.events.forEach(eventName => {
        document.removeEventListener(
          eventName, 
          el._clickOutsideHandler, 
          config.capture
        )
      })
      
      // 清理引用
      delete el._clickOutsideConfig
      delete el._clickOutsideHandler
    }
  }
}

// 用法示例
// <div v-click-outside="closeMenu">菜单内容</div>
// <div v-click-outside.advanced="{ handler: closeMenu, enabled: isOpen }">
// <div v-click-outside:except="{ handler: closeMenu, except: ['.ignore-click'] }">
// <div v-click-outside.mousedown.touchstart="closeMenu">

3.3 滚动指令

// 滚动指令集
const scrollDirectives = {
  // 无限滚动指令
  infiniteScroll: {
    mounted(el, binding) {
      const { value: callback, modifiers, arg } = binding
      
      // 配置选项
      const options = {
        distance: 50,           // 触发距离
        delay: 100,            // 防抖延迟
        immediate: true,       // 立即检查
        disabled: false,       // 是否禁用
        direction: 'vertical'  // 滚动方向
      }
      
      // 解析参数
      if (arg === 'distance') {
        options.distance = parseInt(binding.value) || options.distance
      } else if (typeof binding.value === 'object') {
        Object.assign(options, binding.value)
      }
      
      // 存储配置
      el._infiniteScrollOptions = options
      
      // 防抖函数
      let checkTimer = null
      const checkScroll = () => {
        if (options.disabled) return
        
        let isAtEnd = false
        
        if (options.direction === 'vertical') {
          const scrollTop = el.scrollTop
          const scrollHeight = el.scrollHeight
          const clientHeight = el.clientHeight
          
          isAtEnd = scrollHeight - scrollTop - clientHeight <= options.distance
        } else {
          const scrollLeft = el.scrollLeft
          const scrollWidth = el.scrollWidth
          const clientWidth = el.clientWidth
          
          isAtEnd = scrollWidth - scrollLeft - clientWidth <= options.distance
        }
        
        if (isAtEnd) {
          callback()
        }
      }
      
      const debouncedCheck = () => {
        if (checkTimer) clearTimeout(checkTimer)
        checkTimer = setTimeout(checkScroll, options.delay)
      }
      
      // 监听滚动事件
      el._infiniteScrollHandler = debouncedCheck
      el.addEventListener('scroll', el._infiniteScrollHandler)
      
      // 监听窗口大小变化
      el._resizeHandler = debouncedCheck
      window.addEventListener('resize', el._resizeHandler)
      
      // 立即检查一次
      if (options.immediate) {
        setTimeout(checkScroll, 100)
      }
    },
    
    updated(el, binding) {
      const options = el._infiniteScrollOptions
      const newValue = binding.value
      
      // 更新配置
      if (typeof newValue === 'object') {
        Object.assign(options, newValue)
      } else if (typeof newValue === 'function') {
        // 如果传入了新函数,需要更新回调
        // 注意:这里需要重新绑定事件,简化处理
      }
      
      // 强制检查一次
      if (binding.modifiers.force) {
        setTimeout(() => {
          const event = new Event('scroll')
          el.dispatchEvent(event)
        })
      }
    },
    
    beforeUnmount(el) {
      // 清理事件监听
      if (el._infiniteScrollHandler) {
        el.removeEventListener('scroll', el._infiniteScrollHandler)
        delete el._infiniteScrollHandler
      }
      
      if (el._resizeHandler) {
        window.removeEventListener('resize', el._resizeHandler)
        delete el._resizeHandler
      }
      
      delete el._infiniteScrollOptions
    }
  },
  
  // 滚动到元素指令
  scrollTo: {
    mounted(el, binding) {
      const { value, modifiers, arg } = binding
      
      el._scrollToHandler = (event) => {
        event.preventDefault()
        
        const targetSelector = typeof value === 'string' ? value : value?.target
        const options = typeof value === 'object' ? value : {}
        
        const targetElement = targetSelector 
          ? document.querySelector(targetSelector)
          : document.documentElement
        
        if (!targetElement) return
        
        // 滚动配置
        const scrollOptions = {
          behavior: modifiers.smooth ? 'smooth' : 'auto',
          block: arg || 'start',  // start, center, end, nearest
          inline: 'nearest'
        }
        
        // 合并选项
        Object.assign(scrollOptions, options)
        
        // 执行滚动
        targetElement.scrollIntoView(scrollOptions)
        
        // 触发回调
        if (typeof value === 'object' && value.onScroll) {
          value.onScroll(targetElement)
        }
      }
      
      // 添加点击事件
      el.addEventListener('click', el._scrollToHandler)
      
      // 自动滚动(如果配置了)
      if (modifiers.auto) {
        setTimeout(() => {
          el._scrollToHandler(new Event('click'))
        }, value?.delay || 0)
      }
    },
    
    updated(el, binding) {
      // 如果值变化且配置了auto,重新触发
      if (binding.value !== binding.oldValue && binding.modifiers.auto) {
        setTimeout(() => {
          if (el._scrollToHandler) {
            el._scrollToHandler(new Event('click'))
          }
        }, binding.value?.delay || 0)
      }
    },
    
    beforeUnmount(el) {
      if (el._scrollToHandler) {
        el.removeEventListener('click', el._scrollToHandler)
        delete el._scrollToHandler
      }
    }
  }
}

// 用法示例
// <div v-infinite-scroll="loadMore">内容...</div>
// <div v-infinite-scroll.distance="100">自定义距离</div>
// <div v-infinite-scroll="{ handler: loadMore, distance: 100, disabled: isLoading }">
// <button v-scroll-to="'#section'">滚动到章节</button>
// <button v-scroll-to.smooth.auto="{ target: '#section', delay: 500 }">自动滚动</button>

3.4 拖放指令

// 拖放指令
const vDrag = {
  beforeMount(el, binding) {
    const { value, modifiers } = binding
    
    // 默认配置
    const config = {
      data: null,           // 拖拽数据
      effect: 'move',       // 拖拽效果
      disabled: false,      // 是否禁用
      handle: null,         // 拖拽手柄选择器
      ghost: true,          // 显示幽灵图像
      clone: false,         // 克隆元素
      axis: 'both',         // 拖拽轴向:both, x, y
      boundary: null,       // 边界限制
      onStart: null,        // 开始回调
      onMove: null,         // 移动回调
      onEnd: null           // 结束回调
    }
    
    // 合并配置
    if (typeof value === 'object') {
      Object.assign(config, value)
    } else if (value !== undefined) {
      config.data = value
    }
    
    // 处理修饰符
    if (modifiers.copy) config.effect = 'copy'
    if (modifiers.link) config.effect = 'link'
    if (modifiers.x) config.axis = 'x'
    if (modifiers.y) config.axis = 'y'
    if (modifiers.noGhost) config.ghost = false
    if (modifiers.clone) config.clone = true
    
    // 存储配置和状态
    el._dragConfig = config
    el._dragState = {
      isDragging: false,
      startX: 0,
      startY: 0,
      offsetX: 0,
      offsetY: 0,
      clone: null
    }
    
    // 设置元素属性
    el.setAttribute('draggable', !config.disabled)
    
    // 找到拖拽手柄
    const dragHandle = config.handle 
      ? el.querySelector(config.handle) 
      : el
    
    // 事件处理函数
    const onDragStart = (e) => {
      if (config.disabled) {
        e.preventDefault()
        return
      }
      
      el._dragState.isDragging = true
      el._dragState.startX = e.clientX
      el._dragState.startY = e.clientY
      
      // 设置拖拽数据
      if (config.data !== null) {
        const dataString = typeof config.data === 'string' 
          ? config.data 
          : JSON.stringify(config.data)
        
        e.dataTransfer.setData('application/json', dataString)
        e.dataTransfer.setData('text/plain', dataString)
      }
      
      // 设置拖拽效果
      e.dataTransfer.effectAllowed = config.effect
      
      // 创建幽灵图像
      if (config.ghost) {
        const ghost = el.cloneNode(true)
        ghost.style.opacity = '0.5'
        ghost.style.position = 'absolute'
        ghost.style.top = '-1000px'
        document.body.appendChild(ghost)
        e.dataTransfer.setDragImage(ghost, 0, 0)
        
        // 稍后移除
        setTimeout(() => {
          if (document.body.contains(ghost)) {
            document.body.removeChild(ghost)
          }
        }, 0)
      }
      
      // 克隆元素(如果需要)
      if (config.clone) {
        const clone = el.cloneNode(true)
        clone.style.position = 'absolute'
        clone.style.zIndex = '1000'
        clone.style.pointerEvents = 'none'
        clone.style.opacity = '0.7'
        document.body.appendChild(clone)
        el._dragState.clone = clone
      }
      
      // 触发开始回调
      if (typeof config.onStart === 'function') {
        config.onStart(e, el, config.data)
      }
      
      // 添加拖拽样式
      el.classList.add('dragging')
    }
    
    const onDrag = (e) => {
      if (!el._dragState.isDragging) return
      
      // 计算偏移
      const deltaX = e.clientX - el._dragState.startX
      const deltaY = e.clientY - el._dragState.startY
      
      // 轴向限制
      if (config.axis === 'x') {
        el._dragState.offsetX = deltaX
        el._dragState.offsetY = 0
      } else if (config.axis === 'y') {
        el._dragState.offsetX = 0
        el._dragState.offsetY = deltaY
      } else {
        el._dragState.offsetX = deltaX
        el._dragState.offsetY = deltaY
      }
      
      // 边界限制
      if (config.boundary) {
        const boundary = typeof config.boundary === 'string'
          ? document.querySelector(config.boundary)
          : config.boundary
        
        if (boundary) {
          const boundRect = boundary.getBoundingClientRect()
          const elRect = el.getBoundingClientRect()
          
          // 限制在边界内
          el._dragState.offsetX = Math.max(
            boundRect.left - elRect.left,
            Math.min(el._dragState.offsetX, boundRect.right - elRect.right)
          )
          
          el._dragState.offsetY = Math.max(
            boundRect.top - elRect.top,
            Math.min(el._dragState.offsetY, boundRect.bottom - elRect.bottom)
          )
        }
      }
      
      // 更新克隆元素位置
      if (el._dragState.clone) {
        el._dragState.clone.style.transform = 
          `translate(${el._dragState.offsetX}px, ${el._dragState.offsetY}px)`
      }
      
      // 触发移动回调
      if (typeof config.onMove === 'function') {
        config.onMove(e, el, {
          offsetX: el._dragState.offsetX,
          offsetY: el._dragState.offsetY,
          deltaX,
          deltaY
        })
      }
    }
    
    const onDragEnd = (e) => {
      if (!el._dragState.isDragging) return
      
      el._dragState.isDragging = false
      
      // 移除克隆元素
      if (el._dragState.clone) {
        document.body.removeChild(el._dragState.clone)
        el._dragState.clone = null
      }
      
      // 触发结束回调
      if (typeof config.onEnd === 'function') {
        config.onEnd(e, el, {
          offsetX: el._dragState.offsetX,
          offsetY: el._dragState.offsetY,
          success: e.dataTransfer.dropEffect !== 'none'
        })
      }
      
      // 重置状态
      el._dragState.offsetX = 0
      el._dragState.offsetY = 0
      
      // 移除拖拽样式
      el.classList.remove('dragging')
    }
    
    // 绑定事件
    dragHandle.addEventListener('dragstart', onDragStart)
    dragHandle.addEventListener('drag', onDrag)
    dragHandle.addEventListener('dragend', onDragEnd)
    
    // 存储事件处理函数以便清理
    el._dragHandlers = { onDragStart, onDrag, onDragEnd }
  },
  
  updated(el, binding) {
    const config = el._dragConfig
    const newValue = binding.value
    
    // 更新配置
    if (typeof newValue === 'object') {
      Object.assign(config, newValue)
    }
    
    // 更新 draggable 属性
    el.setAttribute('draggable', !config.disabled)
    
    // 如果禁用状态变化
    if (binding.oldValue?.disabled !== config.disabled) {
      console.log('拖拽指令禁用状态变化:', config.disabled)
    }
  },
  
  beforeUnmount(el) {
    const dragHandle = el._dragConfig?.handle 
      ? el.querySelector(el._dragConfig.handle) 
      : el
    
    const handlers = el._dragHandlers
    
    if (dragHandle && handlers) {
      dragHandle.removeEventListener('dragstart', handlers.onDragStart)
      dragHandle.removeEventListener('drag', handlers.onDrag)
      dragHandle.removeEventListener('dragend', handlers.onDragEnd)
    }
    
    // 清理克隆元素
    if (el._dragState?.clone && document.body.contains(el._dragState.clone)) {
      document.body.removeChild(el._dragState.clone)
    }
    
    // 清理引用
    delete el._dragConfig
    delete el._dragState
    delete el._dragHandlers
  }
}

// 用法示例
// <div v-drag="dragData">可拖拽</div>
// <div v-drag.copy>复制模式</div>
// <div v-drag.x>仅水平拖拽</div>
// <div v-drag="{ data: item, disabled: !isEditable, onEnd: handleDrop }">
// <div v-drag.clone>拖拽时显示克隆</div>
// <div v-drag.no-ghost>不显示幽灵图像</div>

3.5 权限控制指令

// 权限控制指令集
const permissionDirectives = {
  // 角色权限指令
  role: {
    beforeMount(el, binding) {
      const { value, modifiers, arg } = binding
      
      // 获取当前用户角色
      const userRole = getCurrentUserRole()
      
      // 检查权限
      const hasPermission = checkRolePermission(userRole, value, arg)
      
      // 根据权限显示/隐藏元素
      if (!hasPermission) {
        if (modifiers.hide) {
          // 隐藏元素
          el.style.display = 'none'
          el._originalDisplay = el.style.display
        } else if (modifiers.disable) {
          // 禁用元素
          el.disabled = true
          el._originalDisabled = el.disabled
          el.classList.add('disabled')
        } else if (modifiers.remove) {
          // 移除元素
          el.parentNode?.removeChild(el)
        } else {
          // 默认:隐藏元素
          el.style.display = 'none'
          el._originalDisplay = el.style.display
        }
      }
      
      // 存储权限信息
      el._permissionInfo = {
        required: value,
        userRole,
        hasPermission,
        action: arg || 'view'
      }
    },
    
    updated(el, binding) {
      const oldPermission = el._permissionInfo
      const newValue = binding.value
      
      // 重新检查权限
      const userRole = getCurrentUserRole()
      const hasPermission = checkRolePermission(userRole, newValue, binding.arg)
      
      // 如果权限状态变化
      if (oldPermission.hasPermission !== hasPermission) {
        // 恢复原始状态
        if (oldPermission.hasPermission === false) {
          if (binding.modifiers.hide && el._originalDisplay !== undefined) {
            el.style.display = el._originalDisplay
          } else if (binding.modifiers.disable && el._originalDisabled !== undefined) {
            el.disabled = el._originalDisabled
            el.classList.remove('disabled')
          }
        }
        
        // 应用新权限
        if (!hasPermission) {
          if (binding.modifiers.hide) {
            el._originalDisplay = el.style.display
            el.style.display = 'none'
          } else if (binding.modifiers.disable) {
            el._originalDisabled = el.disabled
            el.disabled = true
            el.classList.add('disabled')
          } else if (binding.modifiers.remove) {
            el.parentNode?.removeChild(el)
          }
        }
        
        // 更新权限信息
        el._permissionInfo = {
          required: newValue,
          userRole,
          hasPermission,
          action: binding.arg || 'view'
        }
      }
    },
    
    unmounted(el) {
      // 清理
      delete el._permissionInfo
      delete el._originalDisplay
      delete el._originalDisabled
    }
  },
  
  // 功能权限指令
  feature: {
    mounted(el, binding) {
      const { value, modifiers } = binding
      
      // 检查功能是否启用
      const isEnabled = checkFeatureEnabled(value)
      
      if (!isEnabled) {
        if (modifiers.hide) {
          el.style.display = 'none'
        } else if (modifiers.disable) {
          el.disabled = true
          el.classList.add('disabled')
        } else {
          el.style.opacity = '0.5'
          el.style.pointerEvents = 'none'
        }
      }
      
      el._featureInfo = {
        feature: value,
        enabled: isEnabled
      }
    },
    
    updated(el, binding) {
      const isEnabled = checkFeatureEnabled(binding.value)
      
      if (el._featureInfo.enabled !== isEnabled) {
        if (isEnabled) {
          // 恢复
          if (binding.modifiers.hide) {
            el.style.display = ''
          } else if (binding.modifiers.disable) {
            el.disabled = false
            el.classList.remove('disabled')
          } else {
            el.style.opacity = ''
            el.style.pointerEvents = ''
          }
        } else {
          // 禁用
          if (binding.modifiers.hide) {
            el.style.display = 'none'
          } else if (binding.modifiers.disable) {
            el.disabled = true
            el.classList.add('disabled')
          } else {
            el.style.opacity = '0.5'
            el.style.pointerEvents = 'none'
          }
        }
        
        el._featureInfo = {
          feature: binding.value,
          enabled: isEnabled
        }
      }
    },
    
    unmounted(el) {
      delete el._featureInfo
    }
  }
}

// 工具函数
function getCurrentUserRole() {
  // 从Vuex、Pinia或localStorage获取
  return localStorage.getItem('userRole') || 'guest'
}

function checkRolePermission(userRole, required, action = 'view') {
  // 权限配置
  const permissions = {
    admin: ['create', 'read', 'update', 'delete', 'manage'],
    editor: ['create', 'read', 'update'],
    viewer: ['read'],
    guest: []
  }
  
  // 如果是数组,检查任意一个
  if (Array.isArray(required)) {
    return required.some(role => 
      permissions[userRole]?.includes(action) && role === userRole
    )
  }
  
  // 如果是字符串,精确匹配
  return permissions[userRole]?.includes(action) && required === userRole
}

function checkFeatureEnabled(feature) {
  // 从配置或特性开关获取
  const featureFlags = JSON.parse(localStorage.getItem('featureFlags') || '{}')
  return featureFlags[feature] !== false
}

// 用法示例
// <button v-role="'admin'">仅管理员可见</button>
// <button v-role="['admin', 'editor']">管理员和编辑可见</button>
// <button v-role:edit="'admin'">管理员可编辑</button>
// <button v-role.hide="'admin'">非管理员隐藏</button>
// <button v-role.disable="'admin'">非管理员禁用</button>
// <div v-feature="'newUI'">新UI功能</div>
// <div v-feature.disable="'betaFeature'">测试功能</div>

四、指令组合与复用

4.1 指令组合器

// 指令组合器:将多个指令组合成一个
function createDirectiveComposer(...directives) {
  return {
    beforeMount(...args) {
      directives.forEach(directive => {
        if (directive.beforeMount) directive.beforeMount(...args)
      })
    },
    
    mounted(...args) {
      directives.forEach(directive => {
        if (directive.mounted) directive.mounted(...args)
      })
    },
    
    beforeUpdate(...args) {
      directives.forEach(directive => {
        if (directive.beforeUpdate) directive.beforeUpdate(...args)
      })
    },
    
    updated(...args) {
      directives.forEach(directive => {
        if (directive.updated) directive.updated(...args)
      })
    },
    
    beforeUnmount(...args) {
      directives.forEach(directive => {
        if (directive.beforeUnmount) directive.beforeUnmount(...args)
      })
    },
    
    unmounted(...args) {
      directives.forEach(directive => {
        if (directive.unmounted) directive.unmounted(...args)
      })
    }
  }
}

// 使用示例
const vTooltip = {
  mounted(el, binding) {
    el.title = binding.value
    el.classList.add('has-tooltip')
  },
  unmounted(el) {
    el.classList.remove('has-tooltip')
  }
}

const vHighlight = {
  mounted(el, binding) {
    if (binding.value) {
      el.classList.add('highlight')
    }
  },
  updated(el, binding) {
    if (binding.value) {
      el.classList.add('highlight')
    } else {
      el.classList.remove('highlight')
    }
  }
}

// 组合指令
const vTooltipHighlight = createDirectiveComposer(vTooltip, vHighlight)

// 注册
app.directive('tooltip-highlight', vTooltipHighlight)

// 使用
// <div v-tooltip-highlight="'提示文本'">内容</div>

4.2 指令工厂函数

// 指令工厂:创建可配置的指令
function createResizableDirective(options = {}) {
  const defaultOptions = {
    handles: ['right', 'bottom', 'bottom-right'],
    minWidth: 100,
    minHeight: 100,
    maxWidth: null,
    maxHeight: null,
    onResize: null,
    onResizeStart: null,
    onResizeEnd: null
  }
  
  const config = { ...defaultOptions, ...options }
  
  return {
    mounted(el, binding) {
      const instanceOptions = typeof binding.value === 'object' 
        ? { ...config, ...binding.value }
        : config
      
      // 创建调整大小的手柄
      const handles = instanceOptions.handles
      const handleElements = []
      
      handles.forEach(handle => {
        const handleEl = document.createElement('div')
        handleEl.className = `resize-handle resize-handle-${handle}`
        handleEl.dataset.handle = handle
        
        // 添加事件监听
        handleEl.addEventListener('mousedown', (e) => {
          e.preventDefault()
          e.stopPropagation()
          startResize(e, handle, el, instanceOptions)
        })
        
        el.appendChild(handleEl)
        handleElements.push(handleEl)
      })
      
      // 存储引用
      el._resizeHandles = handleElements
      el._resizeOptions = instanceOptions
      
      // 添加可调整大小的样式
      el.classList.add('resizable')
    },
    
    updated(el, binding) {
      // 更新选项
      if (typeof binding.value === 'object') {
        Object.assign(el._resizeOptions, binding.value)
      }
    },
    
    beforeUnmount(el) {
      // 清理手柄
      if (el._resizeHandles) {
        el._resizeHandles.forEach(handle => {
          handle.removeEventListener('mousedown', handle._resizeHandler)
          el.removeChild(handle)
        })
        delete el._resizeHandles
      }
      
      delete el._resizeOptions
      el.classList.remove('resizable')
    }
  }
}

// 调整大小逻辑
function startResize(e, handle, el, options) {
  const startX = e.clientX
  const startY = e.clientY
  const startWidth = el.offsetWidth
  const startHeight = el.offsetHeight
  
  // 触发开始回调
  if (typeof options.onResizeStart === 'function') {
    options.onResizeStart(e, el, { width: startWidth, height: startHeight })
  }
  
  // 鼠标移动处理
  const onMouseMove = (e) => {
    const deltaX = e.clientX - startX
    const deltaY = e.clientY - startY
    
    let newWidth = startWidth
    let newHeight = startHeight
    
    // 根据手柄类型计算新尺寸
    if (handle.includes('right')) {
      newWidth = Math.max(options.minWidth, startWidth + deltaX)
      if (options.maxWidth) {
        newWidth = Math.min(options.maxWidth, newWidth)
      }
    }
    
    if (handle.includes('bottom')) {
      newHeight = Math.max(options.minHeight, startHeight + deltaY)
      if (options.maxHeight) {
        newHeight = Math.min(options.maxHeight, newHeight)
      }
    }
    
    // 应用新尺寸
    el.style.width = `${newWidth}px`
    el.style.height = `${newHeight}px`
    
    // 触发调整回调
    if (typeof options.onResize === 'function') {
      options.onResize(e, el, { width: newWidth, height: newHeight })
    }
  }
  
  // 鼠标抬起处理
  const onMouseUp = (e) => {
    document.removeEventListener('mousemove', onMouseMove)
    document.removeEventListener('mouseup', onMouseUp)
    
    // 触发结束回调
    if (typeof options.onResizeEnd === 'function') {
      options.onResizeEnd(e, el, {
        width: el.offsetWidth,
        height: el.offsetHeight
      })
    }
  }
  
  // 添加全局事件监听
  document.addEventListener('mousemove', onMouseMove)
  document.addEventListener('mouseup', onMouseUp)
}

// 创建不同配置的指令
const vResizable = createResizableDirective()
const vResizableHorizontal = createResizableDirective({ handles: ['right'] })
const vResizableVertical = createResizableDirective({ handles: ['bottom'] })

// 用法示例
// <div v-resizable>可调整大小</div>
// <div v-resizable="{ minWidth: 200, maxWidth: 800, onResize: handleResize }">
// <div v-resizable.horizontal>仅水平调整</div>

五、最佳实践与注意事项

5.1 性能优化

// 1. 使用防抖和节流
import { debounce, throttle } from 'lodash'

const vScrollOptimized = {
  mounted(el, binding) {
    const handler = binding.value
    
    // 使用防抖
    const debouncedHandler = debounce(handler, 300, {
      leading: true,
      trailing: true
    })
    
    // 使用节流
    const throttledHandler = throttle(handler, 100)
    
    el._scrollHandler = binding.modifiers.debounce 
      ? debouncedHandler
      : binding.modifiers.throttle
        ? throttledHandler
        : handler
    
    el.addEventListener('scroll', el._scrollHandler)
  },
  
  beforeUnmount(el) {
    el.removeEventListener('scroll', el._scrollHandler)
  }
}

// 2. 事件委托
const vEventDelegation = {
  mounted(el, binding) {
    const eventType = binding.arg || 'click'
    const selector = binding.value.selector
    const handler = binding.value.handler
    
    el._delegationHandler = (e) => {
      // 检查事件目标是否匹配选择器
      if (e.target.matches(selector) || e.target.closest(selector)) {
        handler(e)
      }
    }
    
    el.addEventListener(eventType, el._delegationHandler)
  },
  
  beforeUnmount(el) {
    el.removeEventListener(binding.arg || 'click', el._delegationHandler)
  }
}

// 3. 指令复用
// 创建可复用的基础指令
const baseDirective = (customHooks = {}) => ({
  beforeMount(el, binding, vnode) {
    // 公共前置逻辑
    console.log('指令绑定到元素:', el.tagName)
    
    // 调用自定义钩子
    if (customHooks.beforeMount) {
      customHooks.beforeMount(el, binding, vnode)
    }
  },
  
  mounted(el, binding, vnode) {
    // 公共逻辑
    el.dataset.directiveMounted = 'true'
    
    // 调用自定义钩子
    if (customHooks.mounted) {
      customHooks.mounted(el, binding, vnode)
    }
  },
  
  // ... 其他钩子
})

// 创建特定指令
const vCustomDirective = baseDirective({
  mounted(el, binding) {
    // 特定逻辑
    el.style.color = binding.value
  }
})

5.2 错误处理

// 指令错误处理
const vSafeDirective = {
  mounted(el, binding) {
    try {
      // 可能出错的操作
      const result = JSON.parse(binding.value)
      el._parsedData = result
    } catch (error) {
      // 错误处理
      console.error('指令执行错误:', error)
      
      // 显示错误状态
      el.classList.add('directive-error')
      el.title = `指令错误: ${error.message}`
      
      // 触发错误事件
      el.dispatchEvent(new CustomEvent('directive-error', {
        detail: { error, binding }
      }))
    }
  },
  
  updated(el, binding) {
    // 检查值是否有效
    if (binding.value === undefined || binding.value === null) {
      console.warn('指令接收到无效值')
      return
    }
    
    // 继续执行
    this.mounted(el, binding)
  },
  
  beforeUnmount(el) {
    // 清理
    el.classList.remove('directive-error')
    delete el._parsedData
  }
}

六、总结

6.1 生命周期钩子关键点

// 记忆口诀
const directiveLifecycle = `
口诀一:
挂载前准备(beforeMount)
挂载后执行(mounted)
更新前检查(beforeUpdate)
更新后响应(updated)
卸载前清理(beforeUnmount)
卸载后释放(unmounted)

口诀二:
绑定看 beforeMount + mounted
更新看 beforeUpdate + updated
解绑看 beforeUnmount + unmounted

口诀三:
Vue2 转 Vue3:
bind → beforeMount
inserted → mounted
update → beforeUpdate
componentUpdated → updated
unbind → beforeUnmount + unmounted
`

// 使用建议
const bestPractices = `
1. 在 mounted 中操作 DOM
2. 在 updated 中响应数据变化
3. 在 beforeUnmount/unmounted 中清理资源
4. 使用指令参数和修饰符增强功能
5. 考虑性能,合理使用事件监听
6. 保持指令的单一职责
7. 添加错误处理
8. 提供清理函数避免内存泄漏
`

6.2 完整示例:图片懒加载指令

const vLazyLoad = {
  beforeMount(el, binding) {
    // 初始化
    el._lazyLoadObserver = null
    el._lazyLoadSrc = binding.value
    el._lazyLoadOptions = {
      root: null,
      rootMargin: '50px',
      threshold: 0.1,
      ...(typeof binding.value === 'object' ? binding.value.options : {})
    }
  },
  
  mounted(el, binding) {
    // 设置占位符
    const placeholder = el.getAttribute('data-src') || '/placeholder.jpg'
    el.setAttribute('src', placeholder)
    
    // 获取真实图片地址
    const src = typeof binding.value === 'string' 
      ? binding.value 
      : binding.value.src
    
    // 如果图片已经在视窗内,直接加载
    if (isElementInViewport(el)) {
      loadImage(el, src)
      return
    }
    
    // 使用 IntersectionObserver 懒加载
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          loadImage(el, src)
          observer.unobserve(el)
        }
      })
    }, el._lazyLoadOptions)
    
    observer.observe(el)
    el._lazyLoadObserver = observer
  },
  
  updated(el, binding) {
    // 如果图片地址变化
    const newSrc = typeof binding.value === 'string' 
      ? binding.value 
      : binding.value.src
    
    if (newSrc !== el._lazyLoadSrc) {
      // 停止观察
      if (el._lazyLoadObserver) {
        el._lazyLoadObserver.unobserve(el)
        el._lazyLoadObserver = null
      }
      
      // 重新设置
      el._lazyLoadSrc = newSrc
      
      // 如果已经在视窗内,直接加载
      if (isElementInViewport(el)) {
        loadImage(el, newSrc)
      } else {
        // 重新观察
        const observer = new IntersectionObserver((entries) => {
          entries.forEach(entry => {
            if (entry.isIntersecting) {
              loadImage(el, newSrc)
              observer.unobserve(el)
            }
          })
        }, el._lazyLoadOptions)
        
        observer.observe(el)
        el._lazyLoadObserver = observer
      }
    }
  },
  
  beforeUnmount(el) {
    // 清理 IntersectionObserver
    if (el._lazyLoadObserver) {
      el._lazyLoadObserver.unobserve(el)
      el._lazyLoadObserver = null
    }
  }
}

// 工具函数
function isElementInViewport(el) {
  const rect = el.getBoundingClientRect()
  return (
    rect.top <= (window.innerHeight || document.documentElement.clientHeight) &&
    rect.left <= (window.innerWidth || document.documentElement.clientWidth) &&
    rect.bottom >= 0 &&
    rect.right >= 0
  )
}

function loadImage(el, src) {
  const img = new Image()
  
  img.onload = () => {
    el.setAttribute('src', src)
    el.classList.add('loaded')
    
    // 触发加载完成事件
    el.dispatchEvent(new CustomEvent('lazyloaded', {
      detail: { src, element: el }
    }))
  }
  
  img.onerror = () => {
    console.error(`图片加载失败: ${src}`)
    el.classList.add('error')
    
    // 触发错误事件
    el.dispatchEvent(new CustomEvent('lazyloaderror', {
      detail: { src, element: el }
    }))
  }
  
  img.src = src
}

// 用法示例
// <img v-lazy-load="'/images/picture.jpg'">
// <img v-lazy-load="{ src: '/images/picture.jpg', options: { rootMargin: '100px' } }">

核心要点:

  1. Vue 3 指令有 6 个生命周期钩子:beforeMount、mounted、beforeUpdate、updated、beforeUnmount、unmounted
  2. 正确选择钩子:根据操作类型选择合适的时间点
  3. 资源管理:在卸载钩子中清理事件监听、定时器、观察器等
  4. 性能优化:合理使用防抖、节流、事件委托
  5. 错误处理:增强指令的健壮性

掌握指令生命周期钩子,你可以创建强大、可复用、高性能的自定义指令,极大地扩展 Vue 的能力边界!

【 前端三剑客-39 /Lesson65(2025-12-12)】从基础几何图形到方向符号的演进与应用📐➡️🪜➡️🥧➡️⭕➡️🛞➡️🧭

作者 Jing_Rainbow
2026年1月15日 18:22

📐➡️🪜➡️🥧➡️⭕➡️🛞➡️🧭在数学、工程、设计乃至日常生活中,几何图形不仅是抽象思维的载体,更是人类理解空间、构建结构、传递信息的重要工具。从最简单的三角形开始,经过梯形、扇形、圆形、椭圆,最终演化为具有方向意义的箭头,这一序列不仅体现了图形复杂度的递增,也反映了功能从静态描述向动态指示的转变。本文将深入探讨这些图形的定义、性质、应用场景及其相互之间的联系,力求全面而详尽。


📐 三角形(Triangle)

三角形是由三条线段首尾相连所围成的平面图形,是最基本的多边形之一。其内角和恒为180°,这是欧几里得几何中的核心定理之一。

分类方式

  • 按边长

    • 等边三角形(三边相等,三个角均为60°)
    • 等腰三角形(两边相等,底角相等)
    • 不等边三角形(三边均不相等)
  • 按角度

    • 锐角三角形(三个角均小于90°)
    • 直角三角形(一个角为90°,满足勾股定理 a2+b2=c2a^2 + b^2 = c^2
    • 钝角三角形(一个角大于90°)

重要性质

  • 稳定性:三角形是唯一具有“刚性”的多边形——在不改变边长的情况下无法变形,因此广泛应用于桥梁、塔架、桁架等工程结构中。

  • 重心、垂心、内心、外心:三角形拥有多个特殊点,分别对应质量中心、高线交点、内切圆圆心、外接圆圆心。

  • 面积公式

    • 基础公式:A=12××A = \frac{1}{2} \times 底 \times 高

应用领域

  • 建筑学(屋顶结构、桁架)
  • 计算机图形学(3D建模的基本单元)
  • 导航与测量(三角测量法)

🪜 梯形(Trapezoid / Trapezium)

梯形是至少有一组对边平行的四边形。在美式英语中称为 trapezoid,而在英式英语中 trapezium 指无平行边的四边形,需注意术语差异。

定义与分类

  • 普通梯形:仅一组对边平行(称为“底”),另两边为“腰”。
  • 等腰梯形:非平行边(腰)长度相等,底角相等,具有对称轴。
  • 直角梯形:其中一个腰垂直于底边,形成两个直角。

性质

  • 中位线(连接两腰中点的线段)长度等于两底之和的一半。
  • 面积公式:A=(上底+下底)×2A = \frac{(上底 + 下底) \times 高}{2}

应用

  • 土木工程(堤坝横截面、渠道设计)
  • 机械设计(滑块导轨、楔形结构)
  • 艺术构图(营造透视感或稳定感)

梯形可视为三角形的“扩展”——若将三角形顶部截去一部分,即得梯形。这种“截断”操作在几何变换中十分常见。


🥧 扇形(Sector of a Circle)

扇形是由圆心角及其所对的弧与两条半径围成的图形,形如披萨切片,故常用🍕表示,但此处更强调其几何属性,故用🥧(派)象征。

构成要素

  • 圆心角 θ\theta(可用度数或弧度表示)

特殊情形

应用场景

  • 仪表盘设计(速度表、电量显示)
  • 统计图表(饼图 Pie Chart 的基本单元)
  • 光学(激光束发散角、照明覆盖区域)
  • 机械(齿轮齿形、凸轮轮廓)

扇形是从圆形“切割”而来,体现了从整体到局部的分析思维。


⭕ 圆形(Circle)

圆形是平面上到定点(圆心)距离等于定长(半径)的所有点的集合,具有完美的对称性。

核心性质

  • 周长:C=2πrC = 2\pi r
  • 面积:A=πr2A = \pi r^2
  • 任意直径将圆分为两个全等半圆
  • 圆周角定理:同弧所对的圆周角相等,且为圆心角的一半

对称性

  • 无限条对称轴(每条直径都是对称轴)
  • 中心对称图形

应用

  • 车轮(滚动摩擦最小)
  • 齿轮传动(均匀受力)
  • 天文轨道(理想化模型)
  • 信号传播(波前呈圆形扩散)

圆形是所有封闭曲线中,在给定周长下面积最大的图形(等周定理),体现了自然界的效率最优。


🛞 椭圆(Ellipse)

椭圆是平面上到两个定点(焦点)的距离之和为常数的点的轨迹。当两焦点重合时,椭圆退化为圆形。

数学表达

  • 离心率:e=cae = \frac{c}{a}0<e<10 < e < 1e=0e=0 时为圆

几何性质

  • 两条对称轴(长轴与短轴)
  • 反射性质:从一焦点发出的光线经椭圆反射后必过另一焦点(用于声学聚焦、医疗碎石)

实际应用

  • 行星轨道(开普勒第一定律)
  • 建筑穹顶(如美国国会大厦)
  • 光学镜面(椭圆反射镜)
  • 工程制图(斜截圆柱的截面为椭圆)

椭圆可视为圆形在某一方向上的“拉伸”或“压缩”,是仿射变换下的不变图形。


🧭 箭头(Arrow)

箭头并非传统几何图形,而是一种符号性图形,用于表示方向、流程、趋势或指示。

结构成分

  • 杆部(Shaft) :通常为直线或带状,表示路径或主轴。
  • 箭头头(Head) :多为三角形或锥形,强调方向终点。
  • 有时包含尾羽(如弓箭样式),增强视觉识别。

类型与用途

  • 单向箭头:→ 表示单一方向(如流程图、路标)
  • 双向箭头:↔ 表示往返或对等关系
  • 弯曲箭头:↷ 表示旋转、循环或非线性过程
  • 粗/细箭头:表示强度、优先级或数据流大小

设计原则

  • 清晰性:方向必须一目了然
  • 比例协调:箭头头不宜过大或过小
  • 上下文适配:在UI设计、交通标志、数学符号中风格各异

与其他图形的联系

  • 箭头头部常采用三角形,因其尖锐指向性强。
  • 在矢量场中,箭头长度代表大小,方向代表矢量方向
  • 在时间线或流程图中,箭头连接圆形(节点)或椭圆(状态),形成信息网络。

箭头标志着从静态几何动态语义的跃迁——它不再仅仅描述“是什么”,而是说明“往哪里去”。


🔗 图形演进的逻辑脉络

从 📐 到 🧭,这一序列隐含着人类认知与技术发展的深层逻辑:

  1. 构建基础(三角形):最稳定的结构单元,支撑物理世界。
  2. 扩展形态(梯形):引入不对称与实用性,适应现实需求。
  3. 引入曲线(扇形):从直线到弧线,开启连续性思维。
  4. 追求完美对称(圆形):理想化的极限形式,体现自然法则。
  5. 打破对称,拥抱真实(椭圆):描述更复杂的自然现象(如轨道)。
  6. 超越形状,传达意图(箭头):图形成为信息载体,服务于沟通与控制。

这一路径不仅是几何复杂度的提升,更是从存在(being)到变化(becoming)的哲学演进。


💎 结语

无论是用于计算桥梁承重的三角形,还是指示网页“返回顶部”的小小箭头,这些图形早已融入人类文明的肌理。它们既是数学语言的字母,也是工程蓝图的像素,更是视觉传达的词汇。理解它们的定义、性质与关联,不仅有助于学术研究,更能提升我们在数字时代解读世界的能力。

正如古希腊人用圆规与直尺探索宇宙秩序,今天的我们,依然在这些看似简单的图形中,寻找结构、美感与真理的统一。

JS-类型转换:从显式“强制”到隐式“魔法”

2026年1月15日 17:12

前言

在 JavaScript 中,类型转换(Type Coercion)既是它的魅力所在,也是许多 Bug 的温床。为什么 [] == ![] 会等于 true?理解了显示与隐式转换的规则,你就能像编译器一样思考。

一、 显式类型转换 (Explicit Conversion)

显式转换是指开发者通过代码明确地将一种类型转换为另一种类型。

1. 转换为字符串 (String)

  • toString() 方法:大多数值都有此方法。

    注意nullundefined 没有这个方法,直接调用会报错。

  • 字符串拼接:与空字符串相加 val + ""

  • String() 构造函数:万能转换,包括 nullundefined

2. 转换为布尔值 (Boolean)

  • Boolean() 包装:手动转换。

  • 双感叹号 !! :利用逻辑非特性快速转换。

    JavaScript

    console.log(!!'hello'); // true
    console.log(!!0);       // false
    

3. 转换为数字 (Number)

  • Number()

    • null \rightarrow 0
    • undefined \rightarrow NaN
    • true \rightarrow 1, false \rightarrow 0
  • parseInt() / parseFloat()

    • 相比 Number() 更加严格,如果参数是 nullundefinedboolean,统统返回 NaN
    • 常用于从字符串中提取数字:parseInt("12.5px") \rightarrow 12

二、 隐式类型转换 (Implicit Conversion)

当运算符两边的数据类型不统一时,JavaScript 会在后台自动完成转换。

1. 逻辑触发:布尔值转换

在以下逻辑语句中,非布尔值会被隐式转换为布尔值:

  • if (...) / while (...) / for (...)
  • 逻辑非 ! :隐式转为布尔并取反。
  • 逻辑与 && 和 逻辑或 || :先将操作数转为布尔值再判断,但要注意它们返回的是原始操作数,而非布尔值。

2. 算术触发:数字转换

除了加法 + 之外的算术运算符,都会强制将两端转为 Number

  • 运算符-, *, /, %, ++, --
  • 一元正负号+a, -a 会尝试将 a 转为数字。

3. “加法 + ”的特殊规则

+ 运算符具有双重身份(数值加法或字符串拼接):

  • 字符串优先:只要其中一个是字符串,另一个就会转成字符串,然后拼接。
  • 数字优先:如果两个操作数都不是字符串,则都转为数字(或 NaN)进行运算。

三、 对象转基本类型的底层逻辑

当对象参与运算或转换时,JS 引擎会遵循以下流程:

  1. Symbol.toPrimitive:如果对象定义了这个方法,优先调用。
  2. valueOf() :如果没有 toPrimitive,通常先尝试获取原始值。
  3. toString() :如果 valueOf 没能返回基本类型,则调用 toString

JavaScript

// 自定义转换行为
const obj = {
  valueOf: () => 10,
  toString: () => "obj"
};
console.log(obj + 1); // 11 (优先调用 valueOf)

四、 避坑小结:布尔判断中的对象

  • 所有对象(包括空数组 [] 和空对象 {})在转换为布尔值时,结果均为 true
  • 在验证 nullundefined 时,始终建议使用全等 ===,以避免隐式转换带来的干扰。

五、 进阶:经典面试题深度推导

为了验证你是否掌握了前面的知识,我们来看这几个面试高频题:

1. 为什么 [] == ![] 结果是 true

这道题几乎涵盖了所有的隐式转换规则,推导过程如下:

  1. 右侧优先处理![]。由于 [] 是对象,转为布尔值为 true,取反后得到 false

    • 表达式变为:[] == false
  2. 类型不统一:一边是对象,一边是布尔值。根据规则,布尔值先转为数字false 转为 0

    • 表达式变为:[] == 0
  3. 对象转基本类型[] 会尝试调用 valueOf(返回自身)和 toString[].toString() 得到空字符串 ""

    • 表达式变为:"" == 0
  4. 字符串转数字:空字符串 "" 转为数字 0

    • 表达式变为:0 == 0
  5. 结果true

2. 1 + "2" vs 1 - "2"

  • 1 + "2" :遇到 + 且有字符串,触发字符串拼接,结果为 "12"
  • 1 - "2"- 运算符只能用于数值计算,强制将 "2" 转为数字 2,结果为 -1

3. NaN 的奇特逻辑

JavaScript

console.log(NaN == NaN); // false
  • 解析NaN(Not a Number)是 JavaScript 中唯一一个不等于自身的值。
  • 避坑:判断一个值是否是 NaN,请使用 Number.isNaN(),不要直接用 ==

🍮实现一个“果冻”动画的标签栏

作者 冻梨
2026年1月15日 16:54

不管是我们日常使用手机,还是浏览各大平台的官网,都发现动画无处不在。尽管一个好的交互动画,可能不会让用户第一时间察觉到开发者的小心思😭,但当用户对比之后就会发现,好像是比别人多了点东西😮。而这,就是动画细节。通过这篇文章,我将讲解如何实现一个具有"果冻"效果的动画标签栏,并深入探讨其中的技术细节。

界面设计

  • 标签栏:包含四个白色图标按钮以及装载按钮的标签栏容器。
  • 果冻背景(动画主题):一个白色圆矩形,出现在激活后的图标后,此时图标为黑色。

🤔直接把实现代码搬上来。

这里通过设置mix-blend-mode属性为difference来实现图标与果冻背景的相斥色。difference模式的计算方式是背景色元素颜色背景色-元素颜色。使用该属性的好处是图标会自动根据背景调整颜色,后续动画执行时无需额外调整图标颜色,并且不管果冻背景移动哪,图标只会更改与果冻背景重叠的区域,提高视觉一致性🤞。

交互设计

平滑过渡

首先,我们需要实现这样的一个交互效果——点击某个图标后,果冻背景会丝滑平移到激活的图标处。这里我们可以通过用transform来实现平移,并且使用transition来监测transform的变化,当transform改变时,自动在旧值和新值之间创建平滑过渡。

🙅‍不要使用设置left来实现动画,left属性更改后会触发回流,且每次改变left都需要CPU重新计算布局,CPU还要负责其他任务,容易造成卡顿。使用transform会将元素提升到一个独立的合成层,使用GPU处理图形计算,动画更流畅👌。

实现效果

20260115152309_rec_.gif

果冻拉伸

实现了丝滑平移之后,我们要开始思考,“果冻”是怎么样的。果冻有较强的弹性,会呈现以下状态:

  • 刚平移时:果冻的一端开始平移时,刚开始另一端的速度没那么快,果冻会被拉伸,也就是宽度变大,高度变小。
  • 平移中:维持拉伸状态。
  • 平移停止时:当要停下来时,一端开始减速,另一端速度还没减下来,果冻会被挤压,宽度变小,高度变大。
  • 平移停止后:维持原状。

那现在问题出现了,如果我们添加形变动画,则需要设置transform属性的scale调整缩放,但如果设置了scale,则会覆盖掉负责平移的translateX,这怎么解决呢🤔?

image.png 我有个点子🤓!既然一个元素无法添加两个transform,那我们可以用一个元素将果冻背景包裹,让这个元素负责丝滑平移,果冻背景本身负责呈现弹性,用户无法看到包裹元素,只会看到果冻背景一边拉伸🥴一边平移。废话不多说,直接上代码。

emmm总感觉还是缺点啥。

image.png

不好,脑子里要进东西了!回忆侵占我的脑海。

那是一个晴朗的下午,我在掘金上看到了几篇关于贝塞尔曲线在动画中的应用文章,当时大惊,原来贝塞尔曲线还能这样玩出花,学到了学到了。

我知道缺什么了,缺了点“duang”

image.png

“duang”的关键——贝塞尔曲线

关于贝塞尔曲线,大家可以看这篇文章——CSS动画中的贝塞尔曲线,这位博主写的很好,结合图形动画一看了就懂。

简单来说,贝塞尔曲线在css中可以应用到transition的transition-timing-function和animation的animation-timing-function。通过使用贝塞尔曲线,我们可以设置元素在动画过程中的运动状态。

cubic-bezier(x1, y1, x2, y2);

cubic-bezier()函数定义了一个三次贝塞尔曲线,起点(0,0)和终点(1,1)已设置,我们通过调整中间两个点来绘制不同的贝塞尔曲线,这里给大家安利一个网站——cubic-bezier.com,这个网站我们可以可视化拖拽两点来生成贝塞尔曲线。

进入正题,我们能用贝塞尔曲线做什么呢?在这里,我们可以使用这样的一个曲线,让果冻在停止运动过程中,超出原平移位置,并恢复到原位置。

image.png 大家应该猜到我想要做的了,在现实生活中,一个有弹性的物品停止时,会因为弹性导致往回位移一段距离,这是实现“duang”的关键,之前只通过形变的方式是形到意未到。

transition: transform 0.5s cubic-bezier(0.68, 0, 0.26, 1.25);

通过更改transition的代码,即可实现,最终效果如下:

20260115164624_rec_.gif最终实现的代码并不一定就是完美代码,更多细节需要开发者投入更多的心思去完善。

感谢观看!欢迎各位大佬指正

使用Web Worker的经历

作者 刘羡阳
2026年1月15日 15:59

什么是webWorker

  • webworker实现了前端开发的多线程开发,就是把js代码放到后台线程中跑
  • 运行独立线程,不会互相影响
  • 纯消息通知,如果用过websocket的话,使用过程中就会发现这个跟websocket类似的

-初始化webWorker

class DataMonitor {
  private options: DataMonitorOptions;
  private worker: Worker | null;


  constructor(options: DataMonitorOptions) {
    this.options = options;
    this.worker = null;
  }

  // 初始化Web Worker
  initWorker() {
    try {
      // 初始化Web Worker
      this.worker = new Worker(new URL('./data-worker.ts', import.meta.url), { type: 'module' });
      // 监听Worker消息
      this.options.onLog('Worker初始化成功', 'success');
    } catch (error: unknown) {
      this.options.onLog(`Worker初始化失败: ${error instanceof Error ? error.message : String(error)}`, 'error');
    }
  }
}
  • 这段代码就是newWorker的初始化,我在这里面传递了两个参数
    • 第一个参数就是我要进行数据通信的文件
    • 第二个参数中的type为module是为了让worker中使用import/export这种语句,并且可以导入其他的es5模块

进行通信

  1. 通信有两种通信情况
 - 第一种是我在DataMonitor中创建的方法,我会在外部通过事件触发的形式来触发这个方法,
 然后通过里面 this.worker.postMessage({type: '...'})这里的type是自己定义的通信类型
 ,当我触发这个通信的时候就会通知data-worker.ts中的这个通信类型,然后根据这个类型去调用对应的方法
 - 简单示例
     addChat: (data: any) => { //这个是在DataMonitor类中定义的方法
      if (this.worker) {
        this.worker.postMessage({
          type: 'getTopic',
        })
      }
    }
//data-worker.ts
    self.onmessage = function (event) {
      switch (event.data.type) {
        case 'addChat':
          messageHandlers.addChat(event.data);
          break;
      }
    }
- 第二种就是当我调用data-work中定义的方法后,如果成功或者失败如何通知主线程,其实调用方法跟上面的一致
self.postMessage({
    type: 'addFileOrIns',
    data: {
      res: 'error',
      type: 'addFileOrIns',
      id: newId,
      timestamp: new Date().toISOString(),
    }
  })这里的self就是对应上面this.worke
  • 这里有个问题需要注意一下,这里每次通信传递的数据类型,就是data里面带的数据,不能是复杂数据类型的,如果想传复杂数据类型的话可以通过两种形式来传

    • 第一种就是把复杂类型转化成字符串进行传递,JSON.stringify(newItem)这种写法,这种写法方便快捷,但是有个问题就是当数据量过大时传输效率太低了,适合不是很复杂的数据类型
    • 第二种就是通过把数据类型先转换成字符串,在转化成二进制数据流进行传递,这样操作的比较麻烦,但是数据传输速度上面要快很多
  • 然后我就可以在class中和data-worke中定义一一对应的消息通知,只有触发到对应的消息通知,才会调用对应的方法

  • data-worker文件做为初始化的时候调用的文件,其实可以类似于一个入口文件,当他的业务复杂的时候可以把业务抽离成单独的文件,然后在data-worker中进行调用

此处放下全部代码

// 数据监控类,封装所有监控相关的方法



// 监控配置类型
interface MonitorConfig {
  interval: number | string;
  dataSource: string;
  apiUrl?: string | null;
  threshold: number | string;
}

// 状态变化数据类型
interface StatusChangeData {
  isMonitoring?: boolean;
  isPaused?: boolean;
  startTime?: Date;
}

// Worker消息事件类型
interface WorkerMessageEvent {
  data: {
    type: string;
    data?: any;
    timestamp?: number;
  };
}

// 回调函数选项类型
interface DataMonitorOptions {
  config: MonitorConfig;
  onLog: (message: string, level: string, timestamp?: number) => void;
  onStatusChange: (status: StatusChangeData) => void;

  onDataUpdate: (data: any) => void;
  onUptimeUpdate: (uptime: string) => void;
}

export class DataMonitor {
  private options: DataMonitorOptions;
  private worker: Worker | null;
  constructor(options: DataMonitorOptions) {
    this.options = options;
    this.worker = null;
  }

  // 初始化Web Worker
  initWorker() {
    try {
      this.worker = new Worker(new URL('./data-worker.ts', import.meta.url), { type: 'module' });
      // 监听Worker消息
      this.worker.onmessage = this.handleWorkerMessage.bind(this);
      this.worker.onerror = this.handleWorkerError.bind(this);
      this.options.onLog('Worker初始化成功', 'success');
    } catch (error: unknown) {
      this.options.onLog(`Worker初始化失败: ${error instanceof Error ? error.message : String(error)}`, 'error');
    }
  }

  // 处理Worker消息
  handleWorkerMessage(event: WorkerMessageEvent) {
    const { type, data, timestamp } = event.data;
    switch (type) {
      case 'status':
        this.updateStatus(data);
        break;
      case 'data_error':
        this.handleDataError(data);
        break;
      case 'check_result':
        this.handleCheckResult(data);
        break;

      case 'log':
        this.options.onLog(data.message, data.level, timestamp);
        break;

      default:
        this.options.onLog(`未知消息类型: ${type}`, 'warning');
    }
  }

  // 处理Worker错误
  handleWorkerError(error: ErrorEvent) {
    this.options.onLog(`Worker错误: ${error.message}`, 'error');
    this.stopMonitoring();
  }

  // 开始监控
  startMonitoring() {
    if (!this.worker) {
      this.initWorker();
    }
    if (this.worker) {
      this.worker.postMessage({
        type: 'start',
      });
    }
    this.options.onLog(`开始监控 - 间隔: ${this.options.config.interval}ms, 数据源: ${this.options.config.dataSource}`, 'info');
  }

  // 停止监控
  stopMonitoring() {
    if (this.worker) {
      this.worker.postMessage({ type: 'stop' });
    }
    this.options.onLog('监控已停止', 'info');
  }

  // 立即检查一次
  checkNow() {
    if (this.worker) {
      this.worker.postMessage({
        type: 'check_once',
      });
      this.options.onLog('执行单次检查', 'info');
    }
  }

  // 处理数据错误
  handleDataError(data: any) {
    this.options.onLog(`数据错误: ${data.error} (源: ${data.source})`, 'error');
    this.handleError(data);
  }

  // 处理检查结果
  handleCheckResult(data: any) {
    //根据返回的结果去更新对应的数据
    this.updateStatus(data)
  }

  // 更新状态信息
  updateStatus(data: any) {
    this.options.onLog(`更新状态: ${JSON.stringify(data).substring(0, 100)}...`, 'status');
  }

  // 处理错误
  handleError(data: any) {
    this.options.onLog(`执行错误处理: ${data.error}`, 'error');
  }
}
/**
 * 数据监控Worker - 后台定时检查线程
 */


export interface Config {
  interval: number | string;
  dataSource: string;
  apiUrl?: string | null;
  threshold: number | string;
}
let monitoringInterval: number | null = null;
let isActive = false;
let checkCount = 0;


// 监听主线程消息
self.onmessage = function (event) {
  const { type, config: newConfig } = event.data;
  switch (type) {
    case 'start':
      startMonitoring(newConfig);
      break;
    case 'pause':
      pauseMonitoring();
      break;
    case 'stop':
      stopMonitoring();
      break;
    case 'check_once':
      // 执行单次检查

      break
    default:
      sendLog(`未知命令: ${type}`, 'warning');
  }
};

// 开始监控
function startMonitoring(newConfig: Config) {
  // 发送检查结果
  self.postMessage({
    type: 'check_result',
    data: {
      backData: '111',
      timestamp: new Date().toISOString(),
    }
  });
}

// 暂停监控
function pauseMonitoring() {
  if (!isActive) {
    sendLog('监控未运行,无法暂停', 'warning');
    return;
  }

  isActive = false;
  sendStatus('监控已暂停');
  sendLog('监控已暂停', 'warning');
}



// 停止监控
function stopMonitoring() {
  isActive = false;

  if (monitoringInterval) {
    clearInterval(monitoringInterval);
    monitoringInterval = null;
  }

  sendStatus('监控已停止');
  sendLog(`监控已停止,共执行 ${checkCount} 次检查`, 'info');
}

// 执行数据检查


// 获取数据(根据不同的数据源)
async function fetchData(currentConfig) {
  const { dataSource, apiUrl } = currentConfig;

  switch (dataSource) {
    case 'mockApi':
      return fetchMockData();

    case 'localStorage':
      return fetchLocalStorageData();

    case 'websocket':
      return fetchWebSocketData();

    case 'externalApi':
      if (!apiUrl) {
        throw new Error('API地址未配置');
      }
      return fetchExternalApiData(apiUrl);

    default:
      throw new Error(`不支持的数据源: ${dataSource}`);
  }
}

// 模拟API数据
async function fetchMockData() {
  // 模拟API延迟
  await sleep(Math.random() * 1000 + 500);

  // 随机决定是否有数据
  if (Math.random() > 0.3) { // 70%的概率有数据
    return {
      timestamp: new Date().toISOString(),
      data: {
        value: Math.floor(Math.random() * 100),
        items: Array.from({ length: Math.floor(Math.random() * 5) + 1 }, (_, i) => ({
          id: i + 1,
          name: `项目${i + 1}`,
          status: Math.random() > 0.5 ? 'active' : 'inactive'
        })),
        metadata: {
          source: 'mock_api',
          version: '1.0'
        }
      }
    };
  } else {
    // 30%的概率返回空数据
    return null;
  }
}

// 从localStorage获取数据
async function fetchLocalStorageData() {
  await sleep(100); // 模拟微小延迟

  const data = localStorage.getItem('monitor_test_key');

  if (data) {
    return {
      timestamp: new Date().toISOString(),
      data: JSON.parse(data),
      source: 'localStorage'
    };
  }

  return null;
}

// WebSocket数据(模拟)
async function fetchWebSocketData() {
  await sleep(Math.random() * 800 + 200);

  // 模拟WebSocket数据
  const hasData = Math.random() > 0.4;

  if (hasData) {
    return {
      timestamp: new Date().toISOString(),
      data: {
        type: 'ws_update',
        payload: {
          users: Math.floor(Math.random() * 1000),
          messages: Math.floor(Math.random() * 5000),
          connections: Math.floor(Math.random() * 100)
        }
      },
      source: 'websocket'
    };
  }

  return null;
}

// 获取外部API数据
async function fetchExternalApiData(apiUrl: string) {
  // 注意:实际使用中可能会遇到CORS问题
  // 这里添加了模拟实现

  await sleep(Math.random() * 1500 + 500);

  try {
    // 如果是演示,可以模拟响应
    if (apiUrl.includes('example.com')) {
      // 模拟API响应
      return {
        timestamp: new Date().toISOString(),
        data: {
          success: true,
          value: Math.floor(Math.random() * 100),
          message: '数据获取成功'
        },
        source: 'external_api'
      };
    } else {
      // 尝试真实请求(注意CORS)
      const response = await fetch(apiUrl, {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json'
        }
      });

      if (!response.ok) {
        throw new Error(`API请求失败: ${response.status}`);
      }

      const data = await response.json();

      return {
        timestamp: new Date().toISOString(),
        data: data,
        source: 'external_api',
        rawResponse: response
      };
    }
  } catch (error) {
    // 如果真实请求失败,返回模拟数据作为fallback
    sendLog(`外部API请求失败: ${error.message},使用模拟数据`, 'warning');

    return {
      timestamp: new Date().toISOString(),
      data: {
        success: false,
        fallback: true,
        value: Math.floor(Math.random() * 100),
        message: '这是回退数据'
      },
      source: 'external_api_fallback'
    };
  }
}

// 检查数据是否为空
function isDataEmpty(data) {
  if (!data) return true;

  // 检查数据对象是否为空
  if (typeof data === 'object') {
    if (Array.isArray(data) && data.length === 0) return true;
    if (Object.keys(data).length === 0) return true;

    // 检查嵌套的数据字段
    if (data.data === null || data.data === undefined) return true;
    if (typeof data.data === 'object' && Object.keys(data.data).length === 0) return true;
  }

  return false;
}

// 处理找到的数据
function handleDataFound(data, config) {
  sendLog(`发现有效数据 (检查点: ${checkCount})`, 'success');

  // 处理数据验证
  const validatedData = validateData(data, config);

  self.postMessage({
    type: 'data_found',
    data: validatedData,
    timestamp: new Date().toISOString()
  });
}

// 处理空数据
function handleEmptyData(data, config) {
  self.postMessage({
    type: 'data_empty',
    data: {
      checkpoint: checkCount,
      timestamp: new Date().toISOString(),
      reason: '数据为空或无内容',
      config: config.dataSource
    },
    timestamp: new Date().toISOString()
  });
}

// 处理数据错误
function handleDataError(error, config) {
  self.postMessage({
    type: 'data_error',
    data: {
      error: error.message,
      source: config.dataSource,
      checkpoint: checkCount,
      timestamp: new Date().toISOString()
    },
    timestamp: new Date().toISOString()
  });
}

// 验证数据
function validateData(data, config) {
  const validated = { ...data };

  // 添加验证标记
  validated.validated = true;
  validated.validationTime = new Date().toISOString();

  // 简单的数据清洗
  if (validated.data && validated.data.value !== undefined) {
    // 确保数值在合理范围内
    validated.data.value = Math.max(0, Math.min(100, validated.data.value));
  }

  // 添加检查计数
  validated.checkCount = checkCount;

  return validated;
}

// 发送状态更新
function sendStatus(message) {
  self.postMessage({
    type: 'status',
    data: {
      message,
      isActive,
      checkCount,
      timestamp: new Date().toISOString()
    }
  });
}

// 发送日志
function sendLog(message, level = 'info') {
  self.postMessage({
    type: 'log',
    data: {
      message,
      level,
      timestamp: new Date().toISOString()
    }
  });
}

// 工具函数:延迟
function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

// Worker初始化完成
sendLog('数据监控Worker已加载', 'success');
sendStatus('准备就绪');

// 错误处理
self.onerror = function (error) {
  sendLog(`Worker发生错误: ${error.message}`, 'error');
};

总结

个人感觉使用webworker可以把业务代码抽离出来,然后通过通信的方式去进行调用,而且此时的业务代码个个是独立的,他们直接互不影响,但是可以通过通信来进行互相调用,而且它不堵塞主线程,这点用着很舒服。

现代浏览器的工作原理

作者 Gooooo
2026年1月15日 15:05

作者:Addy Osmani

注: 对于那些渴望深入了解浏览器工作原理的人来说,Pavel Panchekha 和 Chris Harrelson 编写的 Browser Engineering 是一个极佳的资源。请务必去看看。本文是对浏览器工作原理的一个概览。

Web 开发者通常将浏览器视为一个黑盒,它能神奇地将 HTML、CSS 和 JavaScript 转换为交互式的 Web 应用程序。事实上,像 Chrome (Chromium)、Firefox (Gecko) 或 Safari (WebKit) 这样的现代 Web 浏览器是一个极其复杂的软件。它协调网络通信、解析并执行代码、利用 GPU 加速渲染图形,并在沙箱进程中隔离内容以确保安全。

本文深入探讨了现代浏览器的工作原理——重点关注 Chromium 的架构和内部机制,同时指出其他引擎的不同之处。我们将探索从网络栈和解析流水线到通过 Blink 进行的渲染过程、通过 V8 进行的 JavaScript 引擎、模块加载、多进程架构、安全沙箱以及开发者工具。目标是提供一个对开发者友好的解释,揭开幕后发生的各种事情。

让我们开始浏览器内部机制的探索之旅。

网络与资源加载

每一次页面加载都始于浏览器的网络栈从 Web 获取资源。当你输入一个 URL 或点击一个链接时,浏览器的 UI 线程(运行在“浏览器进程”中)会启动一个导航请求。

浏览器进程是主要的控制进程,负责管理所有其他进程以及浏览器的用户界面。发生在特定网页标签页之外的所有事情都由浏览器进程控制。

具体步骤包括:

  1. URL 解析和安全检查:浏览器解析 URL 以确定协议(http、https 等)和目标域名。它还会判断输入是搜索查询还是 URL(例如在 Chrome 的地址栏中)。这里可能会检查黑名单等安全功能,以避免钓鱼网站。
  2. DNS 查询:网络栈将域名解析为 IP 地址(除非已有缓存)。这可能涉及联系 DNS 服务器。现代浏览器可能会使用操作系统的 DNS 服务,甚至配置 DNS over HTTPS (DoH),但最终它们会获得主机的 IP。
  3. 建立连接:如果不存在与服务器的开放连接,浏览器会打开一个。对于 HTTPS URL,这包括 TLS 握手,以安全地交换密钥并验证证书。浏览器的网络线程透明地处理 TCP/TLS 设置等协议。
  4. 发送 HTTP 请求:连接建立后,会发送一个 HTTP GET 请求(或其他方法)来获取资源。如果服务器支持,现在的浏览器默认使用 HTTP/2 或 HTTP/3,这允许在同一个连接上多路复用多个资源请求。这通过避免 HTTP/1.1 中每个主机约 6 个并行连接的旧限制来提高性能。例如,使用 HTTP/2,HTML、CSS、JS、图像都可以在一个 TCP/TLS 链路上并发获取;而使用 HTTP/3(基于 QUIC UDP),设置延迟会进一步降低。
  5. 接收响应:服务器返回 HTTP 状态码和头部信息,随后是响应体(HTML 内容、JSON 数据等)。浏览器读取响应流。如果 Content-Type 头部缺失或不正确,浏览器可能需要嗅探 MIME 类型,以决定如何处理内容。例如,如果响应看起来像 HTML 但没有标记,浏览器仍会尝试将其视为 HTML(遵循宽容的 Web 标准)。这里也有安全措施:网络层会检查 Content-Type,并可能拦截可疑的 MIME 不匹配或不允许的跨源数据(Chrome 的 CORB——跨源读取拦截——就是这样一种机制)。浏览器还会咨询安全浏览(Safe Browsing)或类似服务,以拦截已知的恶意负载。
  6. 重定向及后续步骤:如果响应是 HTTP 重定向(例如带有 Location 头部的 301 或 302),网络代码将遵循重定向(在通知 UI 线程后)并向新 URL 重复请求。只有在获得包含实际内容的最终响应后,浏览器才会继续处理该内容。

所有这些步骤都发生在网络栈中,在 Chromium 中,网络栈运行在专门的网络服务(Network Service)中(现在通常是一个独立的进程,作为 Chrome “服务化”努力的一部分)。浏览器进程的网络线程协调底层的套接字通信,并在底层使用操作系统的网络 API。重要的是,这种设计意味着渲染器(将执行页面代码的进程)不会直接访问网络——它请求浏览器进程获取所需内容,这在安全上是一个进步。

推测性加载与资源优化

现代浏览器在网络阶段实现了复杂的性能优化。当你悬停在链接上或开始输入 URL 时,Chrome 会主动执行 DNS 预取或打开 TCP 连接(使用预测器或预连接机制),这样如果你点击,部分延迟就已经被消除了。此外还有 HTTP 缓存:如果资源已缓存且新鲜,网络栈可以从浏览器缓存中满足请求,从而避免网络往返。

  • 预解析扫描器(Preload scanner)运作:Chromium 实现了一个复杂的预解析扫描器,它在主解析器之前对 HTML 标记进行令牌化。当主 HTML 解析器被 CSS 或同步 JavaScript 阻塞时,预解析扫描器会继续检查原始标记,以识别可以并行获取的图像、脚本和样式表等资源。这一机制对现代浏览器的性能至关重要,且无需开发者干预即可自动运行。预解析扫描器无法发现通过 JavaScript 注入的资源,这使得此类资源更有可能被串行加载而非并发加载。
  • 早期提示(Early Hints, HTTP 103)早期提示允许服务器在生成主响应时发送资源提示,使用 HTTP 103 状态码。这使得预连接和预加载提示可以在服务器“思考时间”内发送,从而可能将最大内容绘制(LCP)提高数百毫秒。早期提示仅适用于导航请求,支持预连接和预加载指令,但不支持预取。
  • 推测规则 API(Speculation Rules API)推测规则 API 是一项最近的 Web 标准,允许根据用户交互模式定义动态预取和预渲染 URL 的规则。与传统的链接预取不同,该 API 可以预渲染整个页面,包括执行 JavaScript,从而实现近乎瞬时的加载时间。该 API 在脚本元素或 HTTP 头部中使用 JSON 语法来指定应推测加载的 URL。Chrome 设有限制以防止过度使用,并根据紧急程度设置不同的容量。
  • HTTP/2 和 HTTP/3:大多数基于 Chromium 的浏览器和 Firefox 都完全支持 HTTP/2,HTTP/3(基于 QUIC)也得到了广泛支持(Chrome 为支持的网站默认启用)。这些协议通过允许并发传输和减少握手开销来改善页面加载。从开发者的角度来看,这意味着你可能不再需要精灵图(sprite sheets)或域名发散(domain sharding)技巧——浏览器可以在一个连接上高效地并行获取许多小文件。
  • 资源优先级:浏览器还会对某些资源进行优先级排序。通常,HTML 和 CSS 是高优先级(因为它们阻塞渲染),脚本可能是中等(如果标记为 defer/async 则适当调整),图像可能较低。Chromium 的网络栈分配权重,甚至可以取消或推迟请求,以优先处理初始渲染所需的内容。开发者可以使用 link rel=preloadFetch Priority 来影响资源优先级。

在网络阶段结束时,浏览器已经获得了页面的初始 HTML(假设这是一个 HTML 导航)。此时,Chrome 的浏览器进程会选择一个渲染器进程(Renderer Process)来处理内容。Chrome 通常会与网络请求并行启动一个新的渲染器进程(推测性地),以便在数据到达时做好准备。这个渲染器进程是隔离的(稍后会详细介绍多进程架构),并将接管页面的解析和渲染工作。

一旦响应被完全接收(或在流式传输过程中),浏览器进程就会提交导航:它通知渲染器进程接收字节流并开始处理页面。此时,地址栏会更新,并显示新站点的安全指示器(HTTPS 锁等)。现在,行动转移到了渲染器进程:解析 HTML、加载子资源、执行脚本并绘制页面。

解析 HTML、CSS 和 JavaScript

当渲染器进程接收到 HTML 内容时,其主线程开始根据 HTML 规范对其进行解析。解析 HTML 的结果是 DOM(文档对象模型)——一个代表页面结构的树状对象。解析是增量式的,可以与网络读取交错进行(浏览器以流式方式解析 HTML,因此即使在整个 HTML 文件下载完成之前,DOM 就可以开始构建)。

  • HTML 解析和 DOM 构建:HTML 解析被 HTML 标准定义为一个具有容错性的过程,无论标记多么不规范,它都会生成一个 DOM。这意味着即使你忘记了闭合 </p> 标签或标签嵌套错误,解析器也会隐式修复或调整 DOM 树,使其保持有效。例如,<p>Hello <div>World</div> 会在 DOM 结构中自动在 <div> 之前结束 <p>。解析器为 HTML 中的每个标签或文本创建 DOM 元素和文本节点。每个元素都被放置在一个反映源码嵌套关系的树中。

一个重要的方面是,HTML 解析器在进行过程中可能会遇到需要获取的资源:例如,遇到 <link rel="stylesheet" href="..."> 会促使浏览器请求 CSS 文件(在网络线程上),遇到 <img src="..."> 会触发图像请求。这些与解析并行发生。解析器可以在这些加载发生时继续进行,但有一个巨大的例外:脚本

  • 处理 <script> 标签:如果 HTML 解析器遇到 <script> 标签,它会暂停解析,并且必须在继续之前执行脚本(默认情况下)。这是因为脚本可以使用 document.write() 或其他 DOM 操作来更改页面结构或后续内容。通过在那个点立即执行,浏览器保留了相对于 HTML 的正确操作顺序。因此,解析器将脚本交给 JavaScript 引擎执行,只有当脚本完成(且其所做的任何 DOM 更改都已应用)后,HTML 解析才能恢复。这种脚本执行阻塞行为就是为什么在 head 中包含大型脚本文件会减慢页面渲染的原因——在脚本下载并运行之前,HTML 解析无法继续。

CSS 解析与 CSSOM

在处理 HTML 的同时,CSS 文本也必须被解析成浏览器可以处理的结构——通常称为 CSSOM(CSS 对象模型)。CSSOM 基本上是应用于文档的所有样式(规则、选择器、属性)的表示。浏览器的 CSS 解析器读取 CSS 文件(或 <style> 块)并将其转换为 CSS 规则列表(以及大量的布隆过滤器等,以加速样式匹配)。然后,在构建 DOM 的过程中(或者当 DOM 和 CSSOM 都准备就绪时),浏览器将为每个 DOM 节点计算样式。这一步通常被称为样式匹配(style resolution)或样式计算(style calculation)。浏览器结合 DOM 和 CSSOM 来确定每个元素适用的 CSS 规则以及最终的计算样式(在应用层叠、继承和默认样式之后)。输出通常被概念化为每个 DOM 节点与计算样式的关联(该元素的解析后的最终 CSS 属性,例如元素的颜色、字体、大小等)。

值得注意的是,即使没有任何作者定义的 CSS,每个元素都有默认的浏览器样式(用户代理样式表)。例如,在几乎所有浏览器中,<h1> 都有默认的字体大小和边距。浏览器内置的样式规则以最低优先级应用,它们确保了一些合理的默认呈现。开发者可以在 DevTools 中查看计算样式,以了解元素最终具有哪些 CSS 属性。样式计算步骤使用所有适用的样式(用户代理、用户样式、作者样式)来最终确定每个元素的样式。

  • 渲染阻塞行为:虽然 HTML 解析可以在 CSS 未完全加载的情况下进行,但存在一种渲染阻塞关系:浏览器通常会等待 CSS 加载完成(针对 <head> 中的 CSS)才进行首次渲染。这是因为应用不完整的样式表可能会导致样式未定义内容的闪烁(FOUC)。在实践中,如果 HTML 中一个未标记为 async/defer 的脚本出现在 CSS <link> 之前,它还会等待 CSS 加载完成后再执行脚本(因为脚本可能会通过 DOM API 查询样式信息)。作为经验法则,将样式表链接放在 head 中(它们阻塞渲染但需要尽早加载),并将非关键或大型脚本标记为 defer/async 或放在底部,这样它们就不会延迟 DOM 解析。

现在浏览器拥有了:(1) 从 HTML 构建的 DOM 树,(2) 解析后的 CSS 规则 (CSSOM),以及 (3) 每个 DOM 节点的计算样式。这些共同构成了下一阶段的基础:布局。但在继续之前,我们应该更详细地考虑 JavaScript 方面——特别是 JS 引擎(在 Chrome 中是 V8)如何执行代码。我们提到了脚本阻塞,但当 JS 运行时会发生什么?我们将在后面的章节专门讨论 V8 和 JS 执行的内部机制。目前,假设脚本运行时可能会修改 DOM 或 CSSOM(例如调用 document.createElement 或设置元素样式)。浏览器可能必须通过根据需要重新计算样式或布局来响应这些更改(如果重复执行,可能会产生性能成本)。解析期间脚本的初始运行通常包括设置事件处理程序或操作 DOM(例如模板化)。之后,页面通常会被完全解析,我们进入布局和渲染阶段。

样式与布局

在这个阶段,浏览器的渲染器进程知道了 DOM 的结构和每个元素的计算样式。接下来的问题是:所有这些元素在屏幕上的什么位置?它们有多大?这就是布局(Layout,也称为“重排”或“布局计算”)的工作。在这个阶段,浏览器根据 CSS 规则(流、盒模型、flexbox 或 grid 等)和 DOM 层级结构计算每个元素的几何形状——它们的大小和位置。

  • 布局树构建:浏览器遍历 DOM 树并生成一棵布局树(有时称为渲染树或框架树)。布局树在结构上与 DOM 树相似,但它忽略了非视觉元素(例如 scriptmeta 标签不会产生盒子),并且如果需要,可能会将某些元素拆分为多个盒子(例如,跨越多行的单个 HTML 元素可能对应多个布局盒)。布局树中的每个节点都持有该元素的计算样式,并具有节点内容(文本或图像)以及影响布局的计算属性(如宽度、高度、内边距等)等信息。

在布局过程中,浏览器计算每个元素盒子的确切位置(x, y 坐标)和大小(宽度、高度)。这涉及 CSS 规范定义的算法:例如,在正常的文档流中,块级元素自上而下堆叠,默认情况下每个占用全部宽度,而行内元素在行内流动并根据需要换行。现代布局模式如 flexbox 或 grid 有各自的算法。引擎必须考虑字体度量来换行(因此文本布局涉及测量文本运行),并且必须处理外边距、内边距、边框等。存在许多边缘情况(例如外边距折叠规则、浮动、从流中移除的绝对定位元素等),使得布局成为一个出人意料的复杂过程。即使是“简单”的自上而下布局,也必须计算文本中的换行,这取决于可用宽度和字体大小。浏览器引擎拥有专门的团队和多年的开发经验来准确高效地处理布局。

关于布局树的一些细节:

  • display: none 的元素会从布局树中完全移除(它们不产生任何盒子)。相比之下,仅仅是不可见的元素(例如 visibility: hidden)会获得一个布局盒(占用空间),只是稍后不进行绘制。
  • 生成内容的伪元素(如 ::before::after)包含在布局树中(因为它们确实有视觉盒子)。
  • 布局树节点知道它们的几何形状。例如,一个 <p> 元素的布局节点将知道它相对于视口的位置及其尺寸,并为其内部的每一行或行内盒提供子节点。

布局计算:布局通常是一个递归过程。从根节点(<html> 元素)开始,浏览器计算视口的大小(针对 <html>/<body>),然后在其内部布局子元素,依此类推。许多元素的大小取决于它们的子元素或父元素(例如,容器可能会扩展以适应子元素,或者子元素可能是其父元素宽度的 50%)。布局算法通常需要为浮动或某些复杂的交互进行多次传递,但通常它沿一个方向(自上而下)进行,并在需要时可能进行回溯。

到这一阶段结束时,页面上每个元素的位置和大小都是已知的。我们可以从概念上将页面视为一堆盒子(内部有文本或图像)。但我们仍然没有在屏幕上实际绘制任何东西——那是下一步,绘制

然而,有一个关键概念:布局可能是一项昂贵的操作,尤其是重复执行时。如果 JavaScript 稍后更改了元素的大小或添加了内容,它可能会强制对页面的部分或全部进行重新布局。开发者经常听到关于避免“布局抖动”(layout thrashing)的建议(例如在修改 DOM 后立即在 JS 中读取布局信息,这会强制进行同步重新计算)。浏览器尝试通过标记布局树中哪些部分是“脏”的并仅重新计算这些部分来进行优化。但在最坏的情况下,DOM 高层的更改可能需要为大型页面重新计算整个布局。这就是为什么为了获得更好的性能,应尽量减少昂贵的样式/布局操作。

样式和布局回顾:总结一下,浏览器从 HTML 和 CSS 构建了:

  1. DOM 树——结构和内容。
  2. CSSOM——解析后的 CSS 规则。
  3. 计算样式——将 CSS 规则匹配到每个 DOM 节点的结果。
  4. 布局树——过滤掉非视觉元素的 DOM 树,带有每个节点的几何形状。

每个阶段都建立在前一个阶段的基础上。如果任何阶段发生变化(例如,如果脚本更改了 DOM 或修改了 CSS 属性),后续阶段可能需要更新。例如,如果你更改了元素上的 CSS 类,浏览器可能会为该元素(以及如果继承发生变化时的子元素)重新计算样式,然后如果该样式更改影响了几何形状(如 display 或大小),则可能必须重新进行布局,然后必须重新绘制。这条链意味着布局和绘制依赖于最新的样式,依此类推。我们将在 DevTools 部分讨论这方面的性能影响(因为浏览器提供了工具来查看这些步骤何时发生以及耗时多久)。

布局完成后,我们进入下一个主要阶段:绘制

绘制、合成与 GPU 渲染

绘制(Painting)是将结构化的布局信息转化为屏幕上实际像素的过程。传统上,浏览器会遍历布局树并为每个节点发出绘制命令(“绘制背景,在这些坐标绘制文本,绘制图像”)。现代浏览器在概念上仍然这样做,但它们通常将工作拆分为多个阶段,并利用 GPU 来提高效率。

  • 绘制 / 光栅化:在渲染器的主线程上,布局完成后,Chrome 通过遍历布局树生成绘制记录(paint records,或显示列表)。这基本上是一个带有坐标的绘制操作列表,就像艺术家规划如何绘画场景一样:例如,“在 (x,y) 处绘制宽度为 W、高度为 H、填充颜色为蓝色的矩形,然后在 (x2,y2) 处使用字体 XYZ 绘制文本 'Hello',然后在……处绘制图像”等等。这个列表遵循正确的 z-index 顺序(以便重叠元素正确绘制)。例如,如果一个元素具有更高的 z-index,其绘制命令将排在后面(覆盖在)较低 z-index 的内容之上。浏览器必须考虑堆叠上下文、透明度等,以获得正确的顺序。

过去,浏览器可能只是按顺序直接在屏幕上绘制每个元素。但如果页面的某些部分发生变化,这种方法可能会效率低下(你必须重新绘制所有内容)。现代浏览器通常会记录这些绘制命令,然后使用合成(compositing)步骤来组装最终图像,尤其是在使用 GPU 加速时。

  • 分层与合成:合成是一种优化技术,将页面拆分为多个可以独立处理的层。例如,具有 CSS 变换或动画的定位元素可能会获得自己的层。层就像独立的“草稿画布”——浏览器可以分别光栅化(绘制)每个层,然后合成器可以在屏幕上将它们混合在一起,通常使用 GPU。

在 Chromium 的流水线中,生成绘制记录后,会有一个步骤来构建层树(这对应于哪些元素在哪个层上)。某些层是自动创建的(例如视频元素、画布或具有某些 CSS 的元素将被提升为层),开发者可以通过使用 will-changetransform 等 CSS 属性来提示浏览器创建一个层。层之所以有用,是因为层上的移动或透明度更改可以被合成(即仅重新渲染或移动该层),而无需重新绘制整个页面。然而,过多的层可能会占用大量内存并增加开销,因此浏览器会谨慎选择。

确定层之后,Chrome 的主线程会将其交给合成器线程(Compositor thread)。合成器线程运行在渲染器进程中,但与主线程分离(因此即使主 JS 线程繁忙,它也可以继续工作,这对于平滑滚动和动画非常有用)。合成器线程的工作是接收这些层,将它们光栅化(将绘图转换为实际的像素位图),并将它们合成帧。

合成器随后组装一个合成器帧(compositor frame)——这基本上是发给浏览器进程的一条消息,其中包含构成屏幕的所有四边形(层的切片)、它们的位置等。这个合成器帧通过 IPC 提交回浏览器进程,最终浏览器的 GPU 进程(Chrome 中用于访问 GPU 的独立进程)将接收这些信息并进行显示。浏览器进程自身的 UI(如标签栏)也是通过合成器帧绘制的,它们都在最后一步混合在一起。GPU 进程接收到这些帧,并使用 GPU(通过 OpenGL/DirectX/Metal 等)将它们合成——基本上是在屏幕上的正确位置绘制每个纹理,应用变换等,速度非常快。结果就是你看到的最终图像。

当你滚动或进行动画处理时,这种流水线的优势就显现出来了。例如,滚动页面大多只是在较大的页面纹理上更改视口。合成器只需移动层的位置并请求 GPU 重新绘制进入视图的新部分,而无需主线程重新绘制所有内容。如果动画只是一个变换(例如移动一个属于自己层的元素),合成器线程可以在每一帧更新该元素的位置并生成新帧,而无需涉及主线程或重新运行样式和布局。这就是为什么推荐使用“仅合成”(compositing-only)的动画(更改 transformopacity,它们不会触发布局)以获得更好的性能——即使主线程繁忙,它们也可以以 60 FPS 平滑运行。相比之下,对 heightbackground-color 等属性进行动画处理可能会强制每一帧重新布局或重新绘制,如果主线程跟不上,就会产生卡顿。

简而言之,Chrome 的渲染流水线是:DOM → 样式 → 布局 → 绘制(记录显示项) → 分层 → 光栅化(切片) → 合成 (GPU)。Firefox 的流水线在显示列表阶段之前在概念上是相似的,但通过 WebRender,它跳过了显式的层构建,而是将显示列表发送到 GPU 进程,后者随后使用 GPU 着色器处理几乎所有的绘图。WebKit (Safari) 也使用多线程合成器和通过 macOS 上的“CALayers”进行 GPU 渲染。因此,所有现代引擎都利用 GPU 进行渲染,特别是用于合成和光栅化图形密集型部分,以实现高帧率并减轻 CPU 的负担。

在继续之前,让我们更详细地讨论一下 GPU 的角色。在 Chromium 中,GPU 进程是一个独立的进程,其工作是与图形硬件交互。它接收来自所有渲染器合成器以及浏览器 UI 的绘制命令(大多是高级命令,如“在这些坐标绘制这些纹理”)。然后它将其转换为实际的 GPU API 调用。通过将其隔离在一个进程中,一个导致崩溃的错误 GPU 驱动程序不会拖垮整个浏览器——只会导致 GPU 进程崩溃,而它可以重新启动。此外,它还提供了一个沙箱边界(由于 GPU 处理潜在的不受信任内容,如画布绘图、WebGL 等,驱动程序中曾出现过安全漏洞——在进程外运行它们可以降低风险)。

合成的结果最终被发送到显示器(浏览器运行的操作系统窗口或上下文)。对于每个动画帧(目标是 60fps 或每帧 16.7ms 以获得平滑结果),合成器旨在生成一个帧。如果主线程繁忙(例如 JavaScript 执行时间过长),合成器可能会跳帧或无法更新,从而导致可见的卡顿。开发者工具可以在性能时间线中显示掉帧情况。requestAnimationFrame 等技术将 JS 更新与帧边界对齐,以帮助实现平滑渲染。

总结一下,浏览器的渲染引擎仔细地将页面内容和样式分解为一组几何形状(布局)和绘图指令,然后使用层和 GPU 合成高效地将其转换为你看到的像素。这种复杂的流水线使得 Web 上丰富的图形和动画能够以交互式帧率运行。接下来,我们将窥探 JavaScript 引擎,以了解浏览器如何执行脚本(到目前为止我们一直将其视为黑盒)。

深入 JavaScript 引擎 (V8)

JavaScript 驱动了网页的交互行为。在 Chromium 浏览器中,V8 引擎执行 JavaScript(和 WebAssembly)。了解 V8 的工作原理可以帮助开发者编写高性能的 JS。虽然详尽的深入探讨可以写成一本书,但我们将重点关注 JS 执行流水线的关键阶段:解析/编译代码、执行代码以及管理内存(垃圾回收)。我们还将注意到 V8 如何处理现代特性,如即时编译(JIT)分层和 ES 模块。

现代 V8 解析与编译流水线

  • 后台编译:从 Chrome 66 开始,V8 在后台线程上编译 JavaScript 源代码,这减少了在主线程上花费的编译时间,在典型网站上减少了 5% 到 20%。自 41 版本以来,Chrome 就支持通过 V8 的 StreamedSource API 在后台线程上解析 JavaScript 源文件。V8 可以在从网络下载第一个数据块后立即开始解析 JavaScript 源代码,并在流式传输文件时并行继续解析。几乎所有的脚本编译都发生在后台线程上,只有简短的 AST 内部化和字节码最终确定步骤在脚本执行前发生在主线程上。目前,顶级脚本代码和立即调用的函数表达式在后台线程上编译,而内部函数在首次执行时仍在主线程上延迟编译。
  • 解析与字节码:当遇到 <script> 时(无论是在 HTML 解析期间还是稍后加载),V8 首先解析 JavaScript 源代码。这会生成代码的**抽象语法树(AST)**表示。预解析器(preparser)是解析器的一个副本,它执行跳过函数所需的最低限度工作。它验证函数在语法上是否有效,并生成外部函数正确编译所需的所有信息。当稍后调用预解析的函数时,它会被完全解析并按需编译。

V8 不直接从 AST 进行解释,而是使用一个名为 Ignition 的字节码解释器(2016 年引入)。Ignition 将 JavaScript 编译成紧凑的字节码格式,这基本上是虚拟机的一系列指令。这种初始编译非常快,且字节码相当底层(Ignition 是一个基于寄存器的虚拟机)。目标是快速开始执行代码,并将前期成本降至最低(这对页面加载时间很重要)。

  • AST 内部化过程:AST 内部化涉及在 V8 堆上分配字面量对象(字符串、数字、对象字面量模板),供生成的字节码使用。为了实现后台编译,这一过程被移到了编译流水线的后期,即字节码编译之后,这需要修改以访问嵌入在 AST 中的原始字面量值,而不是堆上的内部化值。
  • 显式编译提示(Explicit Compile Hints):V8 引入了一项名为“显式编译提示”的新功能,允许开发者通过预先编译(eager compilation)指示 V8 在加载时立即解析和编译代码。带有此提示的文件在后台线程上编译,而延迟编译则发生在主线程上。对热门网页的实验显示,在 20 个案例中有 17 个性能得到了提升,前台解析和编译时间平均减少了 630 毫秒。开发者可以使用特殊注释向 JavaScript 文件添加显式编译提示,以便为关键代码路径启用后台线程上的预先编译。
  • 扫描器和解析器优化:V8 的扫描器得到了显著优化,带来了全面的改进:单令牌扫描提高了约 1.4 倍,字符串扫描提高了 1.3 倍,多行注释扫描提高了 2.1 倍,标识符扫描根据长度提高了 1.2-1.5 倍。

当脚本运行时,Ignition 解释字节码并执行程序。解释通常比优化的机器码慢,但它允许引擎开始运行,并收集有关代码行为的分析信息。随着代码运行,V8 收集有关其使用方式的数据:变量类型、哪些函数被频繁调用等。这些信息将用于在后续步骤中使代码运行得更快。

JIT 编译分层

V8 并不止步于解释。它采用了多层即时(Just-In-Time)编译器来加速热点代码。其核心思想是:在运行频繁的代码上投入更多的编译精力以使其更快,同时不浪费时间优化只运行一次的代码。

  1. Ignition(解释字节码)。
  2. Sparkplug:V8 的基准 JIT,称为 Sparkplug(约 2021 年推出)。Sparkplug 接收字节码并快速将其编译为机器码,不进行繁重的优化。这会产生比解释更快的原生代码,但 Sparkplug 不进行深度分析——它的目的是几乎像解释器一样快速启动,但生成的代码运行得更快一些。
  3. Maglev:2023 年,V8 引入了 Maglev,这是一个中层优化编译器,目前已积极部署。Maglev 生成代码的速度比 Sparkplug 慢近 20 倍,但比 TurboFan 快 10 到 100 倍,有效地为那些中等热度但不足以进行 TurboFan 优化的函数填补了空白。当 TurboFan 的编译成本过高时,Maglev 也会发挥作用。从 Chrome M117 开始,Maglev 可以处理许多情况,通过弥合基准 JIT 和最高层 JIT 之间的差距,为在“温”代码(不冷也不超热)中花费时间的 Web 应用带来更快的启动速度。
  4. TurboFan:随着函数或循环被多次执行,V8 将启用其最强大的优化编译器。TurboFan 利用收集到的类型反馈生成高度优化的机器码,应用高级优化(内联函数、消除边界检查等)。如果假设成立,这种优化后的代码可以运行得非常快。

因此,V8 现在实际上有四个执行层:Ignition 解释器、Sparkplug 基准 JIT、Maglev 优化 JIT 和 TurboFan 优化 JIT。这类似于 Java 的 HotSpot VM 具有多个 JIT 级别(C1 和 C2)。引擎可以根据执行概况动态决定优化哪些函数以及何时优化。如果一个函数突然被调用了一百万次,它很可能会被 TurboFan 优化以获得最大速度。

英特尔还开发了配置文件引导的分层(Profile-Guided Tiering),增强了 V8 的效率,在 Speedometer 3 基准测试中带来了约 5% 的提升。最近的 V8 更新包括静态根优化(static roots optimization),允许在编译时准确预测常用对象的内存地址,从而显著提高访问速度。

JIT 优化面临的一个挑战是 JavaScript 是动态类型的。V8 可能会在某些假设下优化代码(例如,这个变量始终是整数)。如果稍后的调用违反了这些假设(例如变量变成了字符串),优化后的代码就是无效的。V8 随后会执行去优化(deoptimization):它回退到较低优化版本(或根据新假设重新生成代码)。这一机制依赖于“内联缓存”和类型反馈来快速适应。去优化的存在意味着如果你的代码具有不可预测的类型,有时无法维持峰值性能,但通常 V8 会尝试处理典型模式(例如函数始终被传递相同类型的对象)。

字节码刷新与内存管理

V8 实现了字节码刷新(bytecode flushing):如果一个函数在多次垃圾回收后仍未使用,其字节码将被回收。再次执行时,解析器使用之前存储的结果更快地重新生成字节码。这一机制对内存管理至关重要,但在边缘情况下可能导致解析不一致。

内存管理(垃圾回收):V8 使用垃圾回收器自动管理 JS 对象的内存。多年来,V8 的 GC 已演变为所谓的 Orinoco GC,它是一个分代的、增量的且并发的垃圾回收器。关键点包括:

  • 分代式:V8 按年龄隔离对象。新对象分配在“新生代”(或“托儿所”)。这些对象通过非常快速的清除算法(scavenging algorithm)频繁回收(将存活对象复制到新空间并回收其余部分)。存活了足够多周期的对象会被提升到“老生代”。
  • 增量式 GC:V8 尽可能以小片断而非一次大停顿来执行垃圾回收。这种增量方法将工作分散开来以避免卡顿。例如,它可以在脚本执行之间交错进行一些标记工作,利用空闲时间。
  • 并行 GC:在多核机器上,V8 也可以在并行线程中执行 GC 的某些部分(如标记或清理)。

最终效果是,V8 团队多年来大幅缩短了 GC 停顿时间,使得垃圾回收在大型应用中也几乎察觉不到。次要 GC(新生代清除)通常发生得非常快。主要 GC(老生代)现在较少发生且大多是并发的。如果你打开 Chrome 的任务管理器或 DevTools 的 Memory 面板,你可能会看到 V8 的堆被分为“Young space”和“Old space”,这反映了这种分代设计。

对于开发者来说,这意味着不需要手动管理内存,但你仍应留意:例如,避免在紧密循环中创建大量短命对象(尽管 V8 非常擅长处理短命对象),并意识到持有大型数据结构会使其留在内存中。DevTools 等工具可以强制执行垃圾回收或记录内存概况以查看内存占用情况。

V8 与 Web API:值得一提的是,V8 涵盖了核心 JavaScript 语言和运行时(执行、标准 JS 对象等),但许多“浏览器 API”(如 DOM 方法、alert()、网络 XHR/fetch 等)并不是 V8 本身的一部分。这些由浏览器提供,并通过绑定(bindings)暴露给 JS。例如,当你调用 document.querySelector 时,它在底层进入了引擎与 C++ DOM 实现的绑定。V8 负责调用 C++ 并获取结果,并且有大量的机制来使这个边界变得快速(Chrome 使用 IDL 来生成高效的绑定)。

在了解了浏览器如何获取资源、解析 HTML/CSS、计算布局、使用 GPU 绘制以及运行 JS 之后,我们已经掌握了加载和渲染页面的全过程。但还有更多值得探索的地方:ES 模块如何处理(因为模块涉及其自身的加载机制)、浏览器的多进程架构是如何组织的,以及沙箱和站点隔离等安全特性如何运作。

模块加载与导入映射 (Import Maps)

与传统的 <script> 标签相比,JavaScript 模块(ES6 模块)引入了不同的加载和执行模型。模块不是一个可能创建全局变量的大型脚本文件,而是显式导入/导出值的文件。让我们看看浏览器(特别是 Chrome 中的 V8)如何加载模块,以及动态 import() 和导入映射等特性如何发挥作用。

  • 静态模块导入:当浏览器遇到 <script type="module" src="main.js"> 时,它将 main.js 视为模块入口点。加载过程如下:浏览器获取 main.js,然后将其解析为 ES 模块。在解析期间,它会发现任何 import 语句(例如 import { foo } from './utils.js';)。浏览器不会立即执行代码,而是构建一个模块依赖图。它将启动获取任何导入的模块(在本例中为 utils.js),并递归地解析每个模块的导入、获取,依此类推。这是异步发生的。只有当整个模块图都被获取并解析后,浏览器才能评估模块。模块脚本本质上是延迟执行的——浏览器在所有依赖项就绪之前不会执行模块代码。然后它按依赖顺序执行它们(确保如果模块 A 导入 B,则 B 先运行)。

这种静态导入过程就是为什么在某些情况下无法从 file:// 加载 ES 模块(除非允许),以及为什么它们默认对跨源脚本要求 CORS 的原因——浏览器正在主动链接和加载多个文件,而不仅仅是在页面中丢入一个 <script>

  • 动态 import():除了静态 import 语句外,ES2020 还引入了 import(moduleSpecifier) 表达式。这允许代码飞速加载模块(返回一个解析为模块导出的 Promise)。例如,你可以根据用户操作执行 const module = await import('./analytics.js'),从而对应用进行代码拆分。在底层,import() 触发浏览器获取请求的模块(及其依赖项,如果尚未加载),然后实例化并执行它,并使用模块命名空间对象解析 Promise。V8 和浏览器在这里协同工作:浏览器的模块加载器处理获取和解析,V8 在就绪后处理编译和执行。动态 import 非常强大,因为它也可以在非模块脚本中使用(例如,内联脚本可以动态导入模块)。它本质上赋予了开发者按需加载 JS 的控制权。与静态导入的区别在于,静态导入是提前解析的(在任何模块代码运行之前,整个图都已加载),而动态 import 的行为更像是在运行时加载新脚本(除了具有模块语义和 Promise)。
  • 导入映射 (Import Maps):浏览器中 ES 模块面临的一个挑战是模块说明符(module specifiers)。在 Node 或打包工具中,你经常通过包名导入(例如 import { compile } from 'react')。在 Web 上,如果没有打包工具,'react' 不是一个有效的 URL——浏览器会将其视为相对路径(这会失败)。这就是导入映射发挥作用的地方。导入映射是一个 JSON 配置,告诉浏览器如何将模块说明符解析为真实的 URL。它通过 HTML 中的 <script type="importmap"> 标签提供。例如,导入映射可能会说明说明符 "react" 映射到 "https://cdn.example.com/react@19.0.0/index.js"(指向实际脚本的完整 URL)。然后,当任何模块执行 import 'react' 时,浏览器使用该映射找到 URL 并加载它。本质上,导入映射允许“裸”说明符(如包名)通过映射到 CDN URL 或本地路径在 Web 上工作。

导入映射一直是未打包开发的规则改变者。自 2023 年以来,所有主流浏览器(Chrome 89+、Firefox 108+、Safari 16.4+——所有三个引擎)都支持导入映射。它们对于本地开发或你希望在没有构建步骤的情况下使用模块的简单应用特别有用。对于生产环境,大型应用通常仍会为了性能进行打包(以减少请求数量),但随着浏览器和 HTTP/2/3 的改进,提供许多小模块变得更加可行。

因此,浏览器中的模块加载器由以下部分组成:模块映射(跟踪已加载的内容)、可能的导入映射(用于自定义解析)以及获取/解析逻辑。一旦获取并编译,模块代码将在严格模式下执行,并具有自己的顶级作用域(除非显式附加,否则不会泄露到 window)。导出会被缓存,因此如果另一个模块稍后导入相同的模块,它不会重新运行(它重用已经评估过的模块记录)。

还有一点值得一提,ES 模块与脚本不同,它延迟执行且对于给定的图按顺序执行。如果 main.js 导入 util.js,而 util.js 导入 dep.js,评估顺序将是:dep.js 先运行,然后是 util.js,最后是 main.js(深度优先,后序遍历)。这种确定性的顺序在某些情况下可以避免对 DOMContentLoaded 的需求,因为当你的主模块运行时,其所有导入都已加载并执行。

从 V8 的角度来看,模块由相同的编译流水线处理,但它们创建了独立的 ModuleRecords。引擎确保模块的顶级代码仅在所有依赖项就绪后运行。V8 还必须处理循环模块导入(这是允许的,并可能导致部分初始化的导出)。细节遵循规范——但本质上,引擎将创建所有模块实例,然后通过给它们占位符来解决循环,然后按尊重依赖关系的顺序执行(规范算法是模块图的“DAG”拓扑排序)。

总结一下,浏览器中的模块加载是网络(获取模块文件)、模块解析器(使用导入映射或标准 URL 解析)和 JS 引擎(按正确顺序编译和评估模块)之间的一场协调舞蹈。它比旧的 <script> 加载更复杂,但带来了更模块化和可维护的代码结构。对于开发者来说,关键要点是:使用模块来组织代码,如果你想要裸导入则使用导入映射,并知道你可以通过 import() 在需要时动态加载模块。浏览器将处理确保一切按正确顺序执行的繁重工作。

现在我们已经了解了单个页面的内部机制,让我们放大视野,检查允许多个页面、标签页和 Web 应用同时运行而互不干扰的浏览器架构。这带我们进入了多进程模型。

浏览器多进程架构

现代浏览器(Chrome、Firefox、Safari、Edge 等)都使用多进程架构来实现稳定性、安全性和性能隔离。不同于将整个浏览器作为一个巨大的进程运行(早期浏览器就是这样做的),浏览器的不同方面运行在不同的进程中。Chrome 在 2008 年率先采用了这种方法,其他浏览器也以各种形式效仿。让我们重点关注 Chromium 的架构,并指出 Firefox 和 Safari 的不同之处。

在 Chromium(Chrome、Edge、Brave 等)中,有一个核心的浏览器进程(Browser Process)。该浏览器进程负责 UI(地址栏、书签、菜单——所有浏览器外壳部分),并负责协调资源加载和导航等高级任务。当你打开 Chrome 并在操作系统任务管理器中看到一个条目时,那就是浏览器进程。它也是产生其他进程的父进程。

然后,对于每个标签页(有时是标签页中的每个站点),Chrome 会创建一个渲染器进程(Renderer Process)。渲染器进程为该标签页的内容运行 Blink 渲染引擎和 V8 JS 引擎。通常,每个标签页至少获得一个渲染器进程。

如果你打开了多个不相关的站点,它们将位于不同的进程中(站点 A 在一个进程,站点 B 在另一个进程,依此类推)。Chrome 甚至将跨源 iframe 隔离到单独的进程中(稍后在站点隔离中详细介绍)。渲染器进程是沙箱化的,不能直接任意访问你的文件系统或网络——它必须通过浏览器进程进行这些特权操作。

Chrome 中的其他关键进程包括:

  • GPU 进程:专门用于与 GPU 通信的进程(如前所述)。来自渲染器合成器的所有渲染和合成请求都发送到 GPU 进程,由其发出实际的图形 API 调用。该进程是沙箱化的且独立的,因此 GPU 崩溃不会导致渲染器崩溃。
  • 网络进程:在旧版 Chrome 中,网络是浏览器进程中的一个线程,但现在通过“服务化”通常是一个独立的进程。该进程处理网络请求、DNS 等,并且可以单独进行沙箱化。
  • 实用程序进程(Utility Processes):这些用于 Chrome 可能卸载的各种服务(如音频播放、图像解码等)。
  • 插件进程:在 Flash 和 NPAPI 插件时代,插件运行在自己的进程中。Flash 现在已弃用,因此这不太相关,但架构仍为插件不在主浏览器进程中运行做好了准备。
  • 扩展进程:Chrome 扩展(本质上是可以作用于网页或浏览器的脚本)也运行在独立的进程中,为了安全与网站隔离。

一个简化的视图是:一个浏览器进程协调多个渲染器进程(每个标签页或每个站点实例一个),外加一个 GPU 进程和几个其他服务进程。Chrome 的任务管理器(Windows 上按 Shift+Esc 或通过“更多工具 > 任务管理器”)会列出每个进程类型及其内存使用情况。

多进程的好处

主要好处包括:

  1. 稳定性:如果一个网页(渲染器进程)崩溃或内存泄漏,它不会导致整个浏览器崩溃——你可以关闭该标签页,其余部分保持活跃。在单进程浏览器中,一个糟糕的脚本就可能摧毁一切。当单个标签页的进程死亡时,Chrome 可以显示“喔唷,崩溃了!”错误,你可以独立重新加载它。
  2. 安全(沙箱化):通过在受限进程中运行 Web 内容,浏览器可以限制该代码在系统上能做的事情。即使攻击者在渲染引擎中发现了漏洞,他们也会被困在沙箱中——渲染器进程通常无法读取你的文件,也无法任意打开网络连接或启动程序。它必须向浏览器进程请求文件访问等操作,而这些请求可以被验证或拒绝。这种沙箱是在操作系统级别强制执行的(根据平台使用作业对象、seccomp 过滤器等)。
  3. 性能隔离:一个标签页中的密集工作(沉重的 Web 应用或死循环)大多被限制在该标签页的渲染器进程中。其他标签页(不同进程)可以保持响应,因为它们的进程没有被阻塞。此外,操作系统可以将进程调度到不同的 CPU 核心上——因此两个沉重的页面在多核系统上并行运行的效果比它们是单个进程的线程要好。
  • 站点隔离(Site Isolation):最初,Chrome 的模型是每个标签页一个进程。随着时间的推移,他们将其演变为每个站点一个进程(特别是在 Spectre 之后——见下一节关于安全的内容)。截至 2024 年,站点隔离已在桌面平台的 99% Chrome 用户中默认启用,Android 支持也在不断完善。这意味着如果你有两个标签页都打开了 example.com,Chrome 可能会决定为两者使用同一个进程(为了节省内存,因为它们是同一个站点,放在一起风险较小)。但一个带有 example.comevil.com iframe 的标签页默认会将 evil.com 的 iframe 放在与父页面不同的进程中(以保护 example.com 的数据)。这种强制执行就是 Chrome 所说的“严格站点隔离”(约在 Chrome 67 左右作为默认设置推出)。站点隔离导致 Chrome 由于进程创建增加而多消耗 10-13% 的系统资源,但提供了至关重要的安全收益。

Firefox 的架构称为 Electrolysis (e10s),历史上曾是所有标签页共用一个内容进程(多年来 Firefox 都是单进程,直到 2017 年左右才启用了几个内容进程)。截至 2021 年,Firefox 使用多个内容进程(默认 8 个用于 Web 内容)。通过 Project Fission(站点隔离),Firefox 正在转向类似的站点隔离——它可以为跨站 iframe 开启新进程,并在 Firefox 108+ 中默认启用了站点隔离,将进程数量增加到可能像 Chrome 一样每个站点一个。Firefox 也有一个 GPU 进程(用于 WebRender 和合成)和一个独立的网络进程,类似于 Chrome 的划分。因此在实践中,Firefox 现在拥有一个非常类似 Chrome 的模型。

Safari (WebKit) 同样转向了多进程模型 (WebKit2),其中每个标签页的内容位于独立的 WebContent 进程中,中央 UI 进程控制它们。Safari 的 WebContent 进程也是沙箱化的,如果不通过 UI 进程中介,无法直接访问设备或文件。Safari 还有一个共享的网络进程(可能还有其他助手)。因此,虽然实现细节不同,但概念是一致的:将每个网页的代码隔离在自己的沙箱环境中。

  • 进程间通信 (IPC):这些进程如何互相交谈?浏览器使用 IPC 机制(在 Windows 上通常是命名管道或其他系统 IPC;在 Linux 上可能是 Unix 域套接字或共享内存;Chrome 有自己的 IPC 库 Mojo)。例如,当网络响应到达网络进程时,它需要被交付给正确的渲染器进程(通过浏览器进程协调)。同样,当你执行 DOM fetch() 时,JS 引擎将调用网络 API,该 API 向网络进程发送请求,依此类推。IPC 增加了复杂性,但浏览器进行了大量优化(例如使用共享内存高效传输图像等大数据,并发布异步消息以避免阻塞)。
  • 进程分配策略:Chrome 并不总是为每个标签页都创建一个全新的进程——存在限制(特别是在内存较低的设备上,它可能会为同站标签页重用进程)。如果你打开另一个同站标签页,Chrome 会重用现有的渲染器以节省内存。它还对总进程数有限制(可根据 RAM 大小缩放)。当达到限制时,它可能会开始将多个不相关的站点放在一个进程中,尽管如果启用了站点隔离,它会努力避免混合站点。在 Android 上,由于内存限制,Chrome 使用的进程较少(内容进程通常最多 5-6 个)。
  • 服务化 (Servicification):Chromium 中的另一个概念是将浏览器组件拆分为可以运行在独立进程中的服务。例如,网络服务被制成一个可以进程外运行的独立模块。其理念是模块化——强大的系统可以在各自的进程中运行每个服务,而受限设备可能会将某些服务合并回一个进程以减少开销。

要点:Chromium 的架构旨在将浏览器 UI 和每个站点运行在不同的沙箱中,使用进程作为隔离边界。Firefox 和 Safari 也趋向于类似的设计。这种架构以增加内存使用为代价,极大地提高了安全性和可靠性。Web 内容进程被视为不可信的,这就是站点隔离发挥作用的地方,甚至在独立进程中将不同的源相互隔离。

站点隔离与沙箱化

站点隔离和沙箱化是建立在多进程基础上的安全特性。它们旨在确保即使恶意代码在浏览器中运行,也无法轻易窃取其他站点的数据或访问你的系统。

  • 站点隔离:我们已经提到过——这意味着不同的网站(更严格地说是不同的站点)运行在不同的渲染器进程中。在 2018 年 Spectre 漏洞曝光后,Chrome 的站点隔离得到了加强。Spectre 表明恶意 JavaScript 可能会读取它不应读取的内存(通过利用 CPU 的推测执行)。如果两个站点在同一个进程中,恶意站点就可以利用 Spectre 窥探敏感站点(如银行站点)的内存。唯一稳健的解决方案就是根本不让它们共享进程。因此 Chrome 将站点隔离设为默认:每个站点获得自己的进程,包括跨源 iframe。Firefox 也紧随其后推出了 Project Fission。这与过去相比是一个重大变化,过去如果你有一个父页面和来自不同域的多个 iframe,它们可能都住在同一个进程中。现在,这些 iframe 会被拆分,例如好页面上的 <iframe src="https://evil.com"> 会被强制进入不同的进程,防止即使是底层的攻击在它们之间泄露信息。

从开发者的角度来看,站点隔离大多是透明的。一个影响是嵌入式 iframe 与其父页面之间的通信现在可能跨越进程边界,因此它们之间的 postMessage 在底层是通过 IPC 实现的。但浏览器使这一切变得无缝;你作为开发者只需照常使用 API。

  • 沙箱化:每个渲染器进程(以及其他辅助进程)都运行在权限受限的沙箱中。例如,在 Windows 上,Chrome 使用作业对象并降低权限,使渲染器无法调用大多数访问系统的 Win32 API。在 Linux 上,它使用命名空间和 seccomp 过滤器来限制系统调用。渲染器基本上可以计算和渲染内容,但如果它尝试打开文件、摄像头或麦克风,它将被拦截(除非通过向浏览器进程请求并获得用户许可的正确渠道)。WebKit 的文档明确指出,WebContent 进程无法直接访问文件系统、剪贴板、设备等——它们必须通过中介 UI 进程进行请求。这就是为什么当站点尝试使用你的麦克风时,权限提示是由浏览器 UI(浏览器进程)显示的,如果允许,实际的录音是在受控进程中完成的。沙箱是至关重要的防线。即使攻击者发现了在渲染器中运行原生代码的漏洞,他们随后也会面临沙箱屏障——他们需要第二个漏洞(“逃逸”)才能突破到系统。这种分层方法(称为站点隔离 + 沙箱)是浏览器安全的最高水平。
  • 进程外 iframe (OOPIF):在 Chrome 的站点隔离实现中,他们发明了 OOPIF 这个术语。从用户的角度来看,没有任何变化,但在 Chrome 的内部架构中,页面的每个框架都可能由不同的渲染器进程支持。顶级框架和同站框架共享一个进程;跨站框架使用不同的进程。所有这些进程“协作”渲染单个标签页的内容,由浏览器进程协调。这相当复杂,但 Chrome 有一个可以跨越进程的框架树。这意味着你的一个标签页可能运行着 N 个进程。它们通过 IPC 进行通信,处理跨边界的 DOM 事件或涉及跨上下文的某些 JavaScript 调用。在 Spectre 之后,Web 平台(通过 COOP/COEP、SharedArrayBuffer 等规范)正在考虑这些约束进行演进。
  • 内存与性能成本:站点隔离确实增加了内存使用,因为使用了更多进程。Chrome 开发者指出,在某些情况下可能会有 10-20% 的内存开销。他们通过对同站进行“尽力而为的进程合并”以及限制可产生的进程数量来减轻部分负担。

跨站预取出于隐私原因受到限制,目前仅在用户未为目标站点设置 Cookie 的情况下有效,以防止站点通过可能永远不会被访问的预取页面跟踪用户活动。

总而言之,站点隔离确保了最小权限原则的应用:源 A 的代码无法访问源 B 的数据,除非通过具有明确许可的 Web API(如 postMessage 或分区的存储)。沙箱确保了即使代码是恶意的,它也无法直接触碰你的系统。这些措施使得浏览器漏洞利用变得困难得多——攻击者现在通常需要多个链式漏洞(一个破坏渲染器,一个逃逸沙箱)才能造成严重破坏,这显著提高了门槛。

作为 Web 开发者,你可能不会直接感受到站点隔离,但你通过更安全的 Web 从中受益。需要注意的一点是,跨源交互可能会有略微更多的开销(因为 IPC),并且某些优化(如进程内脚本共享)在跨源之间是不可能的。但浏览器正在不断优化进程间的消息传递,以尽量减少性能影响。

比较 Chromium、Gecko 和 WebKit

我们主要描述了 Chrome/Chromium 的行为(用于 HTML/CSS 的 Blink 引擎,用于 JS 的 V8,通过 Aura/Chromium 基础设施实现的多进程)。其他主要引擎——Mozilla 的 Gecko(用于 Firefox)和 Apple 的 WebKit(用于 Safari)——拥有相同的基本目标和大致相似的流水线,但存在值得注意的差异和历史分歧。

  • 共同概念:所有引擎都将 HTML 解析为 DOM,将 CSS 解析为样式数据,计算布局,并进行绘制/合成。所有引擎都有带有 JIT 和垃圾回收的 JS 引擎。所有现代引擎都是多进程(或至少是多线程)的,以实现并行和安全。

CSS/样式系统的差异

一个有趣的差异是渲染引擎如何实现 CSS 样式计算:

  • Blink (Chromium):在 C++ 中使用单线程样式引擎(历史上基于 WebKit)。它按顺序为 DOM 树计算样式。它具有增量样式失效优化,但总的来说是一个线程在工作(除了动画中的一些微小并行化)。
  • Gecko (Firefox):在 Quantum 项目(2017 年)中,Firefox 集成了 Stylo,这是一个用 Rust 编写的新 CSS 引擎,它是多线程的。Firefox 可以利用所有 CPU 核心并行计算不同 DOM 子树的样式。这是 Gecko 中 CSS 性能的一次重大提升。因此,Firefox 中的样式重新计算可能会使用 4 个核心来完成 Blink 在 1 个核心上完成的工作。这是 Gecko 方法的一个优势(代价是复杂性)。
  • WebKit (Safari):WebKit 的样式引擎像 Blink 一样是单线程的(由于 Blink 在 2013 年从 WebKit 分支出来,它们在那之前共享架构)。WebKit 做了一些有趣的事情,比如为 CSS 选择器匹配开发了字节码 JIT。它可能会将 CSS 选择器转换为字节码,并为了速度 JIT 编译一个匹配器。Blink 没有采用这种做法(它使用迭代匹配)。

因此,在 CSS 方面,Gecko 凭借通过 Rust 实现的并行样式计算脱颖而出。Blink 和 WebKit 依赖于优化的 C++ 以及可能的一些 JIT 技巧(在 WebKit 的情况下)。

布局与图形

所有三个引擎都实现了 CSS 盒模型和布局算法。特定功能可能会先在其中一个落地(例如,WebKit 曾一度在 CSS Grid 支持上领先,随后 Blink 赶上——通常它们通过标准组织共享代码)。

  • Safari (WebKit) 使用的方法与旧版 Chrome 更相似:它有一个带有层的合成器(称为 CALayer,因为在 Mac 和 iOS 上它使用 Core Animation 层)。Safari 很早就转向了 GPU 合成(2009 年的 iPhone OS 和 Safari 4 就已经为某些 CSS 如变换提供了硬件加速合成)。Safari 和 Chrome 虽有分歧,但在概念上都进行切片和合成。Safari 还将大量工作卸载到 GPU(并使用切片,特别是在 iOS 上,切片绘制对于平滑滚动至关重要)。
  • 移动端优化:每个引擎都有针对移动端的特殊情况。例如,WebKit 具有用于滚动的切片覆盖概念(历史上用于 iOS 的 UIWebView)。Android 上的 Chrome 使用“切片”并尝试保持光栅化任务最小化以达到帧率。Firefox 的 WebRender 源自移动优先的 Servo 项目。

JavaScript 引擎

  • V8 (Chromium):如前所述,包含 Ignition、Sparkplug、TurboFan,以及 2023 年加入的 Maglev。
  • SpiderMonkey (Firefox):历史上它有一个解释器,然后是一个基准 JIT 和一个优化 JIT (IonMonkey)。最近的工作 (Warp) 更改了 JIT 分层的工作方式,可能简化了 Ion,并使其更像 TurboFan 使用缓存字节码和类型信息的方法。SpiderMonkey 也有不同的 GC(也是分代的,自 2012 年起称为增量 GC,现在大多是增量/并发的)。
  • JavaScriptCore (Safari):如前所述,它有 4 层(LLInt, Baseline, DFG, FTL)。它使用不同的 GC(WebKit 的 GC 是分代标记-清除,历史上称为 Butterfly 或 Boehm 变体,现在是 bmalloc 等)。JSC 的 FTL 使用 LLVM 进行优化,这是独一无二的(V8 和 SM 有自己的编译器,JSC 在一层中利用了 LLVM)。这可以产生非常快的代码,但编译开销较大。JSC 倾向于在某些基准测试中优先考虑峰值性能。

在 ES 特性方面,得益于 test262 和彼此间的竞争,这三个引擎基本上都紧跟最新标准。

多进程模型差异

  • Chrome:每个标签页通常独立,源级别的站点隔离,进程非常多(可能有几十个)。
  • Firefox:默认进程较少(8 个内容进程处理所有标签页,如果 Fission 需要跨站 iframe 则更多)。因此,它不一定是每个标签页一个进程;标签页在池中共享内容进程。这意味着 Firefox 在多标签场景下内存占用可能较低,但也意味着一个内容进程崩溃可能会波及多个标签页(尽管它尝试按站点分组)。
  • Safari:很可能是每个标签页一个进程(或每几个标签页一个)——在 iOS 上,WKWebView 肯定隔离了每个 webview。Safari 桌面版历史上也是每个标签页独立的。

进程间协调:所有引擎都必须解决类似的问题,例如如何在多进程环境中实现 alert()(它会阻塞 JS)——通常浏览器进程显示 alert UI 并暂停该脚本上下文。存在细微差别(例如,Chrome 并不真正为 alert 阻塞线程——它在渲染器中运行一个嵌套的运行循环,而 Firefox 可能会冻结该标签页的进程)。

崩溃处理:Chrome 和 Firefox 都有崩溃报告器,可以重新启动崩溃的内容进程并在标签页中显示错误。Safari 的 Web 内容进程崩溃通常会在内容区域显示一个更简单的错误消息。

性能权衡

历史上,Chrome 因多进程和 V8 而在 JS 速度和整体性能上备受赞誉。Firefox 通过 Quantum 缩小了许多差距,有时在图形方面超过了 Chrome(WebRender 在复杂页面上可以非常快)。Safari 通常在 Apple 硬件上的图形和低功耗方面表现出色(他们针对功耗进行了大量优化)。

  • 内存:Chrome 以高内存占用著称(所有那些进程)。Firefox 尝试更保守一些。Safari 在 iOS 上出于必要(内存有限)非常节省内存,并且在 WebKit 中进行了大量内存优化。

从 Web 开发者的角度来看,这些差异通常表现为:

  1. 需要在所有引擎上进行测试,因为在 CSS 特性或 API 的实现上可能存在细微差异或错误。
  2. 性能可能不同(例如,由于 JIT 启发式算法,特定的 JS 工作负载在一个引擎中可能比另一个快)。
  3. 某些 API 在其中一个中可能不可用(Safari 通常是最后实现某些新 API 的,如 WebRTC 或 IndexedDB 版本等,尽管它们最终会实现)。

但我们讨论的核心概念(网络 -> 解析 -> 布局 -> 绘制 -> 合成 -> JS 执行)适用于所有引擎,只是内部方法或名称有所不同。

结论与延伸阅读

我们已经走过了现代浏览器内部网页的一生——从输入 URL 的那一刻起,经过网络和导航、HTML 解析、样式、布局、绘制和 JavaScript 执行,一直到 GPU 将像素呈现在屏幕上。我们看到浏览器本质上是微型操作系统:管理进程、线程、内存和一系列复杂的子系统,以确保 Web 内容加载迅速且运行安全。对于 Web 开发者来说,了解这些内部机制可以揭示为什么某些最佳实践(如减少重排或使用异步脚本)对性能至关重要,或者为什么存在某些安全策略(如不在 iframe 中混合源)。

给开发者的几个关键要点:

  1. 优化网络使用:更少的往返和更小的文件 = 更快的初始渲染。浏览器可以做很多事情(HTTP/2、缓存、推测性加载),但你仍应利用资源提示和高效缓存等技术。网络栈性能很高,但延迟永远是杀手。
  2. 为效率构建 HTML/CSS 结构:结构良好的 DOM 和精简的 CSS(避免过深的树或过于复杂的选择器)可以帮助解析和样式系统。理解 CSS 和 DOM 构建计算样式,然后布局计算几何形状——沉重的 DOM 操作或样式更改会触发这些重新计算。
  3. 批量更新 DOM:以避免重复的样式/布局抖动。使用 DevTools 的 Performance 面板来捕捉你的脚本何时导致了多次布局或绘制。
  4. 使用合成友好的 CSS 进行动画:对 transformopacity 的动画保持在主线程之外并在合成器上运行,从而产生平滑的动画。尽可能避免对受布局约束的属性进行动画处理。
  5. 留意 JS 执行:虽然 JS 引擎速度极快,但长任务会阻塞主线程。分解长操作(使页面保持响应),在某些情况下考虑使用 Web Workers 处理后台任务。此外,请记住沉重的 JS 可能会导致 GC 停顿。
  6. 拥抱安全特性:例如在适当的时候使用 iframe 沙箱或 rel=noopener,因为你现在知道浏览器无论如何都会隔离这些;与其配合是件好事。
  7. DevTools 是你的好朋友:特别是性能和网络面板,是查看浏览器具体在做什么的金矿。如果某些东西很慢或卡顿,工具通常会指向原因(长布局、慢绘制等)。

对于那些渴望更深入研究的人,Pavel Panchekha 和 Chris Harrelson 编写的 Browser Engineering 是一个极佳的资源。它基本上是一本免费的在线书籍,引导你构建一个简单的 Web 浏览器,以易于理解的方式涵盖网络、HTML/CSS 解析、布局等内容。

总之,现代浏览器是软件工程的奇迹。它们成功地抽象掉了所有这些复杂性,使得作为开发者,我们大多只需编写 HTML/CSS/JS 并信任浏览器来处理它。然而,通过窥探幕后,我们获得了有助于编写更高性能、更健壮应用的见解。

祝开发愉快!请记住,Web 平台的深度会回馈那些探索它的人——总有更多的东西可以学习,也有工具可以帮助你学习。

延伸阅读

  • Web Browser Engineering —— 浏览器工作原理深度解析书籍。
  • Chromium University —— 关于 Chromium 工作原理的免费深度视频系列,包括精彩的 "Life of a Pixel" 演讲。
  • Inside the Browser (Chrome 开发者博客系列) —— 第 1-4 部分涵盖了架构、导航流、渲染流水线以及输入/控制器线程。
  • Google Chrome at 17 —— 我们浏览器的历史。
  • 本文中的插图由 Susie Lu 委约创作。

Three.js 材质进阶

2026年1月15日 14:47

概述

本文档将介绍Three.js中材质的进阶用法,包括各种高级材质类型、材质属性设置、环境贴图、透明效果等重要概念。通过这些知识点,您将能够创建更加真实和富有表现力的3D场景。

第一部分:MatCap材质

1. MatCap材质的概念

MatCap(Material Capture)材质是一种特殊的材质类型,它预先渲染了材质的光照效果到一张纹理上,因此具有很高的性能效率。MatCap材质非常适合用于实时渲染中需要高质量光照效果但又不想消耗过多性能的场景。

2. 基本使用方法

// 加载MatCap纹理
let matcapTexture = new THREE.TextureLoader().load(
  "./texture/matcaps/54584E_B1BAC5_818B91_A7ACA3-512px.png"
);

// 创建MatCap材质
let material = new THREE.MeshMatcapMaterial({
  matcap: matcapTexture,
  map: preMaterial.map,  // 可以同时使用其他纹理
});

// 应用到网格对象
duckMesh.material = material;

3. MatCap材质的优势

  • 性能优异:无需实时计算光照
  • 效果逼真:预渲染的光照效果非常真实
  • 使用简单:只需要一张纹理即可实现复杂的光照效果

第二部分:Lambert和Phong材质

1. Lambert材质

Lambert材质是一种漫反射材质,它模拟的是理想漫反射表面,适用于不光滑的表面。

let planeMaterial = new THREE.MeshLambertMaterial({
  map: colorTexture,           // 颜色贴图
  specularMap: specularTexture, // 高光贴图
  transparent: true,           // 透明度
  normalMap: normalTexture,    // 法线贴图
  bumpMap: dispTexture,        // 凹凸贴图
  displacementMap: dispTexture, // 位移贴图
  displacementScale: 0.02,     // 位移缩放
  aoMap: aoTexture,            // 环境光遮蔽贴图
});

2. Phong材质

Phong材质是一种更高级的材质,它可以计算镜面高光,适用于光滑表面。

let planeMaterial = new THREE.MeshPhongMaterial({
  map: colorTexture,
  specularMap: specularTexture,
  transparent: true,
  normalMap: normalTexture,
  bumpMap: dispTexture,
  displacementMap: dispTexture,
  displacementScale: 0.02,
  aoMap: aoTexture,
});

3. 两者的区别

  • Lambert材质:只计算漫反射,没有镜面高光
  • Phong材质:计算漫反射和镜面高光,更适合光滑表面

第三部分:Phong材质制作玻璃和水晶效果

1. 玻璃效果的实现

// 使用折射环境贴图
envMap.mapping = THREE.EquirectangularRefractionMapping;

duckMesh.material = new THREE.MeshPhongMaterial({
  map: preMaterial.map,
  refractionRatio: 0.7,   // 折射率
  reflectivity: 0.99,     // 反射率
  envMap: envMap,         // 环境贴图
});

2. 参数调节

  • refractionRatio:控制折射程度,值越大折射越明显
  • reflectivity:控制反射强度,接近1.0时反射效果更强

第四部分:Standard材质详解

Standard材质(MeshStandardMaterial)是Three.js中最常用的PBR(基于物理的渲染)材质之一。

1. 基本属性

// Standard材质的主要属性
const material = new THREE.MeshStandardMaterial({
  color: 0xffffff,              // 基础颜色
  roughness: 0.5,               // 粗糙度 (0-1)
  metalness: 0.5,               // 金属度 (0-1)
  map: texture,                 // 颜色贴图
  normalMap: normalTexture,     // 法线贴图
  roughnessMap: roughnessTexture, // 粗糙度贴图
  metalnessMap: metalnessTexture, // 金属度贴图
  aoMap: aoTexture,             // 环境光遮蔽贴图
  envMap: envMap,               // 环境贴图
});

2. 动态控制材质属性

// 通过GUI动态控制材质属性
let params = {
  aoMap: true,
};

gui.add(params, "aoMap").onChange((value) => {
  mesh.material.aoMap = value ? aoMap : null;
  mesh.material.needsUpdate = true;  // 需要更新材质
});

第五部分:透光性、厚度、衰减颜色和衰减距离

1. 物理材质的透光效果

MeshPhysicalMaterial支持更高级的透光效果,可以模拟玻璃、钻石等材质。

const material = new THREE.MeshPhysicalMaterial({
  transparent: true,                    // 启用透明
  transmission: 0.95,                   // 透射率 (0-1)
  roughness: 0.05,                      // 粗糙度
  thickness: 2,                         // 厚度
  attenuationColor: new THREE.Color(0.9, 0.9, 0), // 衰减颜色
  attenuationDistance: 1,               // 衰减距离
  thicknessMap: thicknessMap,           // 厚度贴图
});

2. 参数说明

  • transmission:透射率,值越高越透明
  • thickness:厚度,影响光线穿过物体的方式
  • attenuationColor:光线在物体内部传播时的颜色变化
  • attenuationDistance:光线在物体内部传播的距离

3. 通过GUI调节参数

gui.add(material, "attenuationDistance", 0, 10).name("衰减距离");
gui.add(material, "thickness", 0, 2).name("厚度");

第六部分:折射率和反射率

1. 折射率(IOR)

折射率(Index of Refraction)决定了光线穿过材质时的弯曲程度。

const material = new THREE.MeshPhysicalMaterial({
  transparent: true,
  transmission: 0.95,
  roughness: 0.05,
  thickness: 2,
  attenuationColor: new THREE.Color(0.9, 0.9, 0),
  attenuationDistance: 1,
  ior: 1.5,                // 折射率
});

// 通过GUI调节折射率
gui.add(material, "ior", 0, 2).name("折射率");

2. 反射率

反射率控制材质表面的反射强度。

// 设置反射率
material.reflectivity = 0.9;

// 通过GUI调节反射率
gui.add(material, "reflectivity", 0, 1).name("反射率");

3. 常见材质的折射率参考

  • 空气:1.0
  • 水:1.33
  • 玻璃:1.5
  • 钻石:2.4

第七部分:清漆、清漆法向和清漆粗糙度

1. 清漆效果

清漆(Clearcoat)属性可以模拟材质表面的额外涂层,如汽车漆面或家具上的清漆层。

const material = new THREE.MeshPhysicalMaterial({
  transparent: true,
  color: 0xffff00,                  // 基础颜色
  roughness: 0.5,                   // 基础粗糙度
  clearcoat: 1,                     // 清漆强度 (0-1)
  clearcoatRoughness: 0,            // 清漆粗糙度 (0-1)
  clearcoatMap: thicknessMap,       // 清漆贴图
  clearcoatRoughnessMap: thicknessMap, // 清漆粗糙度贴图
  clearcoatNormalMap: scratchNormal, // 清漆法线贴图
  normalMap: carbonNormal,          // 基础法线贴图
  clearcoatNormalScale: new THREE.Vector2(0.1, 0.1), // 清漆法线缩放
});

2. 清漆相关属性

  • clearcoat:清漆层的强度
  • clearcoatRoughness:清漆层的粗糙度
  • clearcoatNormalMap:清漆层的法线贴图
  • clearcoatNormalScale:清漆法线贴图的缩放

第八部分:布料和织物材料光泽效果

1. Sheen属性

Sheen属性用于模拟织物等材料的光泽效果,特别适合制作布料、丝绸等材质。

const sphereMaterial = new THREE.MeshPhysicalMaterial({
  color: 0x222288,              // 基础颜色
  sheen: 1,                     // 光泽强度 (0-1)
  sheenColor: 0xffffff,         // 光泽颜色
  roughness: 1,                 // 粗糙度
  sheenRoughness: 1,            // 光泽粗糙度
  sheenColorMap: brickRoughness, // 光泽颜色贴图
});

2. Sheen相关属性

  • sheen:光泽效果的强度
  • sheenColor:光泽的颜色
  • sheenRoughness:光泽的粗糙度
  • sheenColorMap:光泽颜色贴图

第九部分:虹彩效应(Iridescence)

1. 虹彩效果简介

虹彩效果可以模拟某些特殊材质(如肥皂泡、昆虫翅膀、油膜等)产生的彩虹色效果。

const sphereMaterial = new THREE.MeshPhysicalMaterial({
  color: 0xffffff,                           // 基础颜色
  roughness: 0.05,                          // 粗糙度
  transmission: 1,                          // 透射率
  thickness: 0.1,                           // 厚度
  iridescence: 1,                           // 虹彩强度 (0-1)
  reflectivity: 1,                          // 反射率
  iridescenceIOR: 1.3,                      // 虹彩折射率
  iridescenceThicknessRange: [100, 400],    // 虹彩厚度范围
  iridescenceThicknessMap: brickRoughness,  // 虹彩厚度贴图
});

2. 虹彩相关属性

  • iridescence:虹彩效果强度
  • iridescenceIOR:虹彩折射率
  • iridescenceThicknessRange:虹彩厚度范围
  • iridescenceThicknessMap:虹彩厚度贴图

3. 通过GUI调节虹彩参数

// 调节虹彩参数
gui.add(sphereMaterial, "iridescence", 0, 1).name("彩虹色");
gui.add(sphereMaterial, "reflectivity", 0, 1).name("反射率");
gui.add(sphereMaterial, "iridescenceIOR", 0, 3).name("彩虹色折射率");

// 调节虹彩厚度范围
let iridescenceThickness = {
  min: 100,
  max: 400,
};

gui
  .add(iridescenceThickness, "min", 0, 1000)
  .name("彩虹色最小厚度")
  .onChange(() => {
    sphereMaterial.iridescenceThicknessRange[0] = iridescenceThickness.min;
  });

gui
  .add(iridescenceThickness, "max", 0, 1000)
  .name("彩虹色最大厚度")
  .onChange(() => {
    sphereMaterial.iridescenceThicknessRange[1] = iridescenceThickness.max;
  });

第十部分:发光属性与发光贴图

1. 发光效果的实现

通过emissive属性可以创建自发光效果,常用于模拟屏幕、灯泡等发光物体。

// 加载手机模型并设置发光效果
gltfLoader.load(
  "./model/mobile/scene.glb",
  (gltf) => {
    console.log(gltf);
    scene.add(gltf.scene);
    
    // 可以进一步调整屏幕等特定部分的发光效果
    let screen = gltf.scene.getObjectByName("screen"); // 假设屏幕有特定名称
    if(screen) {
      screen.material.emissive = new THREE.Color(0x00ffff); // 设置发光颜色
      screen.material.emissiveIntensity = 1; // 设置发光强度
    }
  }
);

2. 环境贴图的重要性

发光效果通常需要配合高质量的环境贴图才能达到最佳效果:

// 加载HDR环境贴图
let rgbeLoader = new RGBELoader();
rgbeLoader.load("./texture/Alex_Hart-Nature_Lab_Bones_2k.hdr", (envMap) => {
  envMap.mapping = THREE.EquirectangularRefractionMapping;
  scene.background = new THREE.Color(0x7aaff5);
  scene.environment = envMap;  // 设置场景环境贴图
});

第十一部分:控制器限制查看3D场景

1. 轨道控制器的限制设置

为了限制用户的视角范围,可以对轨道控制器设置各种限制:

const controls = new OrbitControls(camera, renderer.domElement);

// 设置目标点
controls.target.set(0, 1.2, 0);

// 禁用平移
controls.enablePan = false;

// 设置距离限制
controls.minDistance = 3;  // 最小距离
controls.maxDistance = 5;  // 最大距离

// 设置角度限制
controls.minPolarAngle = Math.PI / 2 - Math.PI / 12;  // 垂直最小角度
controls.maxPolarAngle = Math.PI / 2;                 // 垂直最大角度

controls.minAzimuthAngle = Math.PI / 2 - Math.PI / 12;  // 水平最小角度
controls.maxAzimuthAngle = Math.PI / 2 + Math.PI / 12;  // 水平最大角度

2. 限制参数说明

  • enablePan:是否启用平移
  • minDistance/maxDistance:相机与目标点的最小/最大距离
  • minPolarAngle/maxPolarAngle:垂直旋转角度限制
  • minAzimuthAngle/maxAzimuthAngle:水平旋转角度限制

第十二部分:材质销毁

1. 资源管理的重要性

在Three.js中,及时释放不再使用的材质、纹理等资源是非常重要的,以避免内存泄漏。

// 销毁物体的示例代码
function disposeObject(obj) {
  if (obj.geometry) {
    obj.geometry.dispose();
  }
  
  if (obj.material) {
    if (Array.isArray(obj.material)) {
      obj.material.forEach(material => {
        if (material.map) material.map.dispose();
        material.dispose();
      });
    } else {
      if (obj.material.map) obj.material.map.dispose();
      obj.material.dispose();
    }
  }
}

2. 纹理和材质的销毁

// 销毁纹理
texture.dispose();

// 销毁材质
material.dispose();

// 销毁几何体
geometry.dispose();

总结

通过本教程,我们学习了Three.js中材质的进阶用法:

  1. MatCap材质:高效预渲染光照效果的材质类型
  2. Lambert和Phong材质:传统光照模型的两种基本材质
  3. 玻璃和水晶效果:利用折射和反射实现透明材质
  4. Standard材质:PBR渲染管线的标准材质
  5. 透光效果:实现半透明和光线穿透效果
  6. 折射率和反射率:控制光线与材质表面的交互
  7. 清漆效果:模拟表面涂层的额外反射层
  8. 光泽效果:模拟织物等材质的特殊光泽
  9. 虹彩效应:创建彩虹色的光学效果
  10. 发光效果:实现自发光材质
  11. 控制器限制:限制用户视角的交互方式
  12. 资源管理:正确销毁材质以避免内存泄漏

这些高级材质特性可以让您的3D场景更加真实和吸引人。掌握这些技术后,您可以创建出令人惊叹的视觉效果。

JS-ES6新特性

2026年1月15日 14:42

前言

ES6 (ECMAScript 2015) 的发布是现代 JavaScript 开发的分水岭。它不仅修复了 var 带来的历史遗留问题,还引入了更高效的数据结构。本文将带你系统复习 let/const、解构赋值、Map/Set 以及独一无二的 Symbol

一、 变量声明的“进化”:let 与 const

在 ES6 之前,我们只有 varm但 var 带来的变量提升和全局污染常常让人头疼,ES6则新增了let、const。

1. let 特点

  • 禁止重复声明:同一作用域内不可重复定义同名变量。
  • 块级作用域:仅在 {} 内部有效(如 iffor 块)。
  • 无变量提升:存在“暂时性死区”(TDZ),必须先定义后使用,否则抛出 ReferenceError

2. const 特点

  • 必须赋初值:声明时必须立即初始化。

  • 值不可变:一旦声明,其指向的内存地址不可修改。

    注意: 修改对象或数组内部的属性是允许的,因为这并没有改变引用地址。

  • 具备块级作用域,同样不存在提升。

3. 三者对比速查表

特性 var let const
作用域 函数作用域 块级作用域 块级作用域
变量提升 是 (显示 undefined) 否 (报错) 否 (报错)
重复声明 允许 不允许 不允许
必须赋初值

二、 解构赋值:代码瘦身的艺术

解构赋值允许我们按照一定模式,从数组和对象中提取值。

1. 数组解构

数组解构是位置对应的。

JavaScript

let [a, [b, c]] = [1, [2, 3]]; // 支持嵌套解构
  • 注意: 如果等号右边不是可遍历结构(Iterator),将会报错。

2. 对象解构

对象解构是属性名对应的,不强调顺序。

JavaScript

let obj = { first: 'hello', last: 'world' };

// 别名用法:{ 原属性名: 新变量名 }
let { first: f, last: l } = obj; 

console.log(f); // 'hello'

3. 函数参数解构

这是开发中最常用的场景,通过设定默认值可以增强代码的健壮性。

JavaScript

function connect({ host = '127.0.0.1', port = 3000 } = {}) {
    console.log(host, port);
}

4. 妙用场景

  • 快速交换变量[x, y] = [y, x]
  • 提取 JSON 数据:从复杂的接口返回对象中精准拿取字段。
  • 接收多个返回值:函数返回数组或对象后直接解构。

三、 键值对的新选择:Map

Map 是一组键值对结构,其查找时间复杂度为 O(1)O(1)

1. Map 的常用 API

  • set(key, value):添加元素。
  • get(key):获取元素。
  • has(key):检查是否存在。
  • delete(key):删除指定元素。
  • size:属性,返回元素个数。
  • clear():清空所有。

2. 核心特性

  • Key 的多样性:对象的 key 只能是字符串或 Symbol,而 Map 的 key 可以是任意类型(包括对象、函数)。
  • 覆盖性:同一个 key 放入多个 value,后面的会覆盖前面的。

JavaScript

const m = new Map();
m.set('Bob', 59);
m.forEach((val, key) => {
    console.log(`${key}: ${val}`);
});

四、 唯一值的容器:Set

Set 类似于数组,但其成员的值都是唯一的。

1. 数组去重的神技

在 ES6 中,一行代码即可搞定数组去重:

JavaScript

let arr = [1, 2, 2, 3];
let uniqueArr = Array.from(new Set(arr)); 
// 或者使用扩展运算符
let uniqueArr2 = [...new Set(arr)];
console.log(uniqueArr,uniqueArr2) //[1, 2, 3],[1, 2, 3]

2. 常用操作

  • add(value):添加新成员。
  • delete(value):删除。
  • has(value):判断是否存在。
  • size:获取长度。

3. 遍历演示

JavaScript

let set = new Set([123, 456, 789]);

for (let item of set) {
   console.log(item); 
}

// 过滤小数值
set.forEach(e => {
    if(e < 500) set.delete(e);
});
console.log(set); // Set { 789 }

五、 独一无二的 Symbol

Symbol 是 ES6 引入的一种原始数据类型,表示独一无二的值。

1. 为什么需要 Symbol?

为了防止对象属性名冲突。如果你给一个他人提供的对象添加属性,使用 Symbol 可以确保不会覆盖原有属性。

2. 基本使用

JavaScript

let s1 = Symbol('desc');
let s2 = Symbol('desc');

console.log(s1 === s2); // false (即使描述相同,值也是唯一的)

// 作为对象属性
let obj = {
    [s1]: 'Hello Symbol'
};

注意Symbol 作为属性名时,通过 for...inObject.keys() 是遍历不到的,需要使用 Object.getOwnPropertySymbols()

Three.js Geometry进阶

2026年1月15日 14:37

概述

本文档将介绍Three.js中Geometry的进阶用法,包括UV映射、法向量、几何体变换、包围盒计算等重要概念。通过这些知识点,您将能够更好地控制和操作3D几何体。

第一部分:UV属性设置

1. UV映射的概念

UV映射是将2D纹理映射到3D几何体表面的过程。U和V分别对应纹理坐标的水平和垂直轴,与3D空间中的X、Y、Z轴相对应。

2. 基本UV设置

// 创建几何体
const geometry = new THREE.BufferGeometry();

// 创建顶点数据
const vertices = new Float32Array([
  -1.0, -1.0, 0.0, 1.0, -1.0, 0.0, 1.0, 1.0, 0.0, -1.0, 1.0, 0,
]);
geometry.setAttribute("position", new THREE.BufferAttribute(vertices, 3));

// 创建索引
const indices = new Uint16Array([0, 1, 2, 2, 3, 0]);
geometry.setIndex(new THREE.BufferAttribute(indices, 1));

// 设置UV坐标
const uv = new Float32Array([
  0, 0,   // 第一个顶点的UV坐标
  1, 0,   // 第二个顶点的UV坐标
  1, 1,   // 第三个顶点的UV坐标
  0, 1,   // 第四个顶点的UV坐标
]);
// 创建UV属性
geometry.setAttribute("uv", new THREE.BufferAttribute(uv, 2));

// 应用纹理
const texture = new THREE.TextureLoader().load("./texture/uv_grid_opengl.jpg");
const material = new THREE.MeshBasicMaterial({ map: texture });
const plane = new THREE.Mesh(geometry, material);

3. UV坐标详解

UV坐标系的原点(0,0)通常位于纹理的左下角,而(1,1)位于右上角。每个顶点都需要分配相应的UV坐标,以便正确映射纹理。

第二部分:法向量

1. 法向量的概念

法向量是垂直于表面的向量,用于确定光照计算、背面剔除等。每个顶点都可以有自己的法向量。

2. 手动设置法向量

// 设置法向量
const normals = new Float32Array([
  0, 0, 1,  // 第一个顶点的法向量
  0, 0, 1,  // 第二个顶点的法向量
  0, 0, 1,  // 第三个顶点的法向量
  0, 0, 1,  // 第四个顶点的法向量
]);
// 创建法向量属性
geometry.setAttribute("normal", new THREE.BufferAttribute(normals, 3));

3. 自动计算法向量

// 计算法向量
geometry.computeVertexNormals();

4. 显示法向量辅助器

import { VertexNormalsHelper } from "three/examples/jsm/helpers/VertexNormalsHelper.js";

// 创建法向量辅助器
const helper = new VertexNormalsHelper(plane, 0.2, 0xff0000);
scene.add(helper);

第三部分:几何体顶点转换

1. 几何体变换方法

BufferGeometry提供了多种变换方法来修改顶点数据:

// 平移几何体
geometry.translate(x, y, z);

// 旋转几何体
geometry.rotateX(Math.PI / 2);
geometry.rotateY(angle);
geometry.rotateZ(angle);

// 缩放几何体
geometry.scale(scaleX, scaleY, scaleZ);

2. 变换的顺序

变换的顺序很重要,通常是先缩放,再旋转,最后平移(SRT顺序)。

第四部分:几何体居中

1. 计算包围盒

// 计算包围盒
geometry.computeBoundingBox();

// 获取包围盒
let boundingBox = geometry.boundingBox;

// 获取包围盒中心点
let center = boundingBox.getCenter(new THREE.Vector3());

2. 几何体居中

// 将几何体居中
geometry.center();

3. 处理模型的世界变换

// 对于复杂模型,需要考虑世界矩阵
mesh.updateWorldMatrix(true, true);
boundingBox.applyMatrix4(mesh.matrixWorld);
let center = boundingBox.getCenter(new THREE.Vector3());

第五部分:包围盒

1. 包围盒的概念

包围盒是包含几何体所有顶点的最小矩形框,常用于碰撞检测和视锥剔除。

2. 计算包围盒

// 计算几何体的边界框
geometry.computeBoundingBox();

// 获取边界框
let boundingBox = geometry.boundingBox;
console.log(boundingBox);

3. 包围球

包围球是包含几何体所有顶点的最小球体:

// 计算包围球
geometry.computeBoundingSphere();

// 获取包围球
let boundingSphere = geometry.boundingSphere;
console.log(boundingSphere);

4. 可视化包围盒

// 创建包围盒辅助器
import { Box3Helper } from "three";

let boxHelper = new THREE.Box3Helper(boundingBox, 0xffff00);
scene.add(boxHelper);

// 创建包围球辅助器
let sphereGeometry = new THREE.SphereGeometry(boundingSphere.radius, 16, 16);
let sphereMaterial = new THREE.MeshBasicMaterial({
  color: 0xff0000,
  wireframe: true,
});
let sphereMesh = new THREE.Mesh(sphereGeometry, sphereMaterial);
sphereMesh.position.copy(boundingSphere.center);
scene.add(sphereMesh);

第六部分:多个几何体合并包围盒

1. 合并包围盒的原理

当需要为多个对象计算一个共同的包围盒时,可以使用union方法。

2. 实现方法

// 创建一个空的包围盒
var box = new THREE.Box3();

// 遍历所有对象
let arrSphere = [sphere1, sphere2, sphere3];
for (let i = 0; i < arrSphere.length; i++) {
  // 方法一:获取单个对象的包围盒并应用世界矩阵
  // arrSphere[i].geometry.computeBoundingBox();
  // let box3 = arrSphere[i].geometry.boundingBox;
  // arrSphere[i].updateWorldMatrix(true, true);
  // box3.applyMatrix4(arrSphere[i].matrixWorld);
  
  // 方法二:直接从对象创建包围盒
  let box3 = new THREE.Box3().setFromObject(arrSphere[i]);
  // 合并包围盒
  box.union(box3);
}

console.log(box);

// 创建合并后的包围盒辅助器
let boxHelper = new THREE.Box3Helper(box, 0xffff00);
scene.add(boxHelper);

第七部分:线框几何体

1. EdgesGeometry

EdgesGeometry用于显示几何体的边缘线条:

// 获取边缘几何体
let edgesGeometry = new THREE.EdgesGeometry(geometry);

// 创建线段材质
let edgesMaterial = new THREE.LineBasicMaterial({
  color: 0xffffff,
});

// 创建线段
let edges = new THREE.LineSegments(edgesGeometry, edgesMaterial);
scene.add(edges);

2. WireframeGeometry

WireframeGeometry用于显示几何体的线框模式:

// 线框几何体
let wireframeGeometry = new THREE.WireframeGeometry(geometry);

// 创建线段
let wireframe = new THREE.LineSegments(wireframeGeometry, edgesMaterial);
scene.add(wireframe);

3. 处理模型的线框显示

对于复杂的GLTF模型,需要遍历所有网格对象:

gltf.scene.traverse((child) => {
  if (child.isMesh) {
    let mesh = child;
    let geometry = mesh.geometry;

    // 获取边缘几何体
    let edgesGeometry = new THREE.EdgesGeometry(geometry);
    let edgesMaterial = new THREE.LineBasicMaterial({
      color: 0xffffff,
    });

    // 创建线段
    let edges = new THREE.LineSegments(edgesGeometry, edgesMaterial);

    // 应用原始对象的变换矩阵
    mesh.updateWorldMatrix(true, true);
    edges.matrix.copy(mesh.matrixWorld);
    edges.matrix.decompose(edges.position, edges.quaternion, edges.scale);

    // 添加到场景
    scene.add(edges);
  }
});

总结

通过本教程,我们学习了Three.js中Geometry的进阶用法:

  1. UV映射:学会了如何为自定义几何体设置UV坐标,使纹理能够正确映射到几何体表面

  2. 法向量:了解了法向量的重要性及其设置方法,包括手动设置和自动计算

  3. 几何体变换:掌握了如何对几何体进行平移、旋转和缩放操作

  4. 几何体居中:学会了如何计算几何体的中心并将其居中

  5. 包围盒和包围球:理解了包围盒和包围球的概念及应用,包括可视化方法

  6. 合并包围盒:掌握了如何为多个对象计算共同的包围盒

  7. 线框几何体:学会了如何显示几何体的边缘和线框,用于模型可视化

这些技能对于创建复杂的3D场景和优化渲染性能至关重要。掌握这些几何体操作技巧将使您能够更精确地控制3D对象的行为和外观。

🤖 用 AI 写 Git Commit Message?我让新手秒变 Git 高手!

2026年1月15日 14:30

“你的 commit message 又写成 fix bug 了?”
“别骂了,我这就去改……”

如果你也曾因提交信息太随意被 Leader 点名批评,或者看着别人优雅的 Git 日志自叹不如——那么恭喜你,今天这篇文章就是为你量身打造的「Git 提交信息拯救计划」!

我们将一起搭建一个 全栈 AI Git Commit Message 生成器,让你每次 git commit 都像 Linus 本人在敲键盘 ✨。


🧠 为什么 Git 提交信息这么重要?

很多人觉得:“代码能跑就行,commit message 随便写写呗。”
但现实很骨感:

  • 项目日志混乱:三个月后回看 update file,你根本不知道改了啥。
  • Code Review 困难:Reviewer 看不懂你意图,只能反复问“这是干啥的?”
  • 绩效考核吃亏:Leader 翻 Git 历史时,看到一串 fix, add, ok,心里默默给你打了个 ❌。
  • 团队协作灾难:新人接手项目,光看 commit log 就想辞职。

规范的 commit message = 专业开发者的第一张名片 💼

而今天,我们要做的,就是让 AI 来帮你写规范、清晰、符合 Conventional Commits 的提交信息


🛠️ 技术栈:全栈 + AI,三端联动

我们的小神器由三部分组成,堪称“三位一体”:

模块 技术 职责
前端 React + Tailwind CSS + Axios 用户界面,展示 AI 生成的 commit message
后端 Node.js + Express 接收请求,调用 AI 模型
AI 引擎 Ollama + DeepSeek-R1:8B(本地 GPU 运行) 真正的“大脑”,理解代码差异并生成高质量 commit

无需联网!无需 OpenAI API!全部跑在你自己的电脑上,隐私安全拉满 🔒


🌐 架构图:数据如何流动?

[前端浏览器] 
    ↓ (POST /chat)
[Express 后端 @3000]
    ↓ (调用 Ollama API)
[Ollama AI @11434 → DeepSeek-R1:8B]
    ↑ (返回 commit message)
[Express 返回 JSON][前端展示结果]

整个过程不到 5 秒,AI 就能根据你的 git diff 输出一条像模像样的 commit:

✅ feat(auth): implement JWT token refresh logic
✅ fix(ui): resolve button overflow on mobile view
✅ docs(readme): update installation instructions

是不是瞬间高大上了?


🧪 后端:Express 的优雅之道

我们用 Express 搭建了一个极简但健壮的 API 服务:

app.post('/chat', async (req, res) => {
  const { message } = req.body;
  if (!message || typeof message !== 'string') {
    return res.status(400).json({ error: "message 必填,必须是字符串" });
  }

  try {
    const prompt = ChatPromptTemplate.fromMessages([
      ['system', '你是一个专业的 Git 提交信息助手,请根据以下代码变更生成符合 Conventional Commits 规范的提交信息。'],
      ['human', '{input}']
    ]);

    const chain = prompt.pipe(model).pipe(new StringOutputParser());
    const result = await chain.invoke({ input: message });

    res.json({ reply: result });
  } catch (e) {
    console.error(e);
    res.status(500).json({ error: "调用大模型失败" });
  }
});

关键点:

  • 输入校验:防止前端乱传数据
  • 错误处理:500 错误不崩服
  • 提示词工程:明确告诉 AI “你要干啥”
  • LangChain 链式调用:模块化、可扩展

Express 虽老,但稳如老狗。配合 cors()express.json() 中间件,跨域和 JSON 解析一键搞定。


🎨 前端:React Hook 封装副作用

我们用自定义 Hook useGitDiff 把 AI 调用逻辑抽离出来:

export const useGitDiff = () => {
  const [content, setContent] = useState('');
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    (async () => {
      setLoading(true);
      const { data } = await chat('请根据以下 git diff 生成 commit message...');
      setContent(data.reply);
      setLoading(false);
    })();
  }, []);

  return { loading, content };
};

组件只需关注 UI:

export default function App() {
  const { loading, content } = useGitDiff();
  return (
    <div className="flex min-h-screen items-center justify-center bg-gray-50">
      {loading ? (
        <p className="text-lg">🧠 AI 正在思考中...</p>
      ) : (
        <pre className="bg-white p-4 rounded shadow-md max-w-2xl">{content}</pre>
      )}
    </div>
  );
}

Tailwind 让样式开发快如闪电,Axios 封装统一 API 调用,清爽又高效!


🤯 跨域?不存在的!

前端跑在 http://localhost:5173,后端在 http://localhost:3000 —— 端口不同,浏览器直接 block 请求。

解决方案?一行代码:

app.use(cors());

CORS 就像给前端办了“外交护照”,浏览器一看:“哦,后端特批了,放行!” 🛂


🧪 如何测试?Apifox 来助阵!

开发时用 ApifoxPostman 直接 POST 到 /chat

{
  "message": "diff --git a/src/App.js b/src/App.js\n+ console.log('hello AI');"
}

秒回:

{ "reply": "feat(app): add debug log in App component" }

调试效率翻倍!


🚀 未来展望:不止于 Commit Message

这个小项目其实是个“AI 开发助手”的雏形。下一步我们可以:

  • 自动读取本地 git diff(通过 Electron 或 CLI 工具)
  • 支持多语言模型切换(Qwen、Llama3、DeepSeek)
  • 集成到 VS Code 插件,一键生成 commit
  • 甚至自动 git commit -m "$(ai-commit)"

💡 结语:让 AI 成为你的编程副驾驶

我们不是要取代开发者,而是把重复、枯燥、容易出错的工作交给 AI,让我们专注在真正有创造力的部分。

一个好的 commit message,不仅是对代码的尊重,更是对团队、对未来的自己负责。

现在,你只需要复制粘贴 AI 生成的内容,就能写出让人眼前一亮的 Git 提交记录——
从此,你的 Git 历史,也能登上“最佳实践”榜单!

第二节:使用Mongoose连接数据库

2026年1月15日 14:22

Mongoose 数据库介绍及安装指南

一、Mongoose 简介

Mongoose 是一个基于 Node.js 的 MongoDB 对象模型工具,它提供了一种更直观的方式来操作 MongoDB 数据库。通过 Mongoose,开发者可以使用面向对象的方式定义数据模型、进行数据验证和建立数据关联。

MongoDB 是一个基于分布式文件存储的 NoSQL 数据库,以其高性能、高可用性和易扩展性而著称。官方地址:www.mongodb.com/

二、MongoDB 安装与配置

下载安装

  1. 获取安装包
    访问官方下载地址:www.mongodb.com/try/downloa…
    建议选择 zip 格式,通用性更强

  2. 安装步骤

    • 将压缩包解压到 C:\Program Files 目录
    • 创建数据存储目录:C:\data\db(MongoDB 默认使用此路径存储数据)
    • 进入 MongoDB 的 bin 目录,打开命令行工具
    • 运行命令启动服务:mongod
  3. 验证启动 当命令行显示 waiting for connections 时,表示 MongoDB 服务已成功启动

MongoDB 启动成功示意图

  1. 连接数据库 在新命令行窗口中运行 mongo 命令即可连接本地 MongoDB 服务

使用建议

  • 环境变量配置:将 MongoDB 的 bin 目录添加到系统环境变量 Path 中,方便全局使用 mongod 命令
  • 操作注意:请勿选中服务端窗口内容,否则会导致服务暂停。如需取消选中,可按回车键

MongoDB 连接示意图

三、Mongoose 连接配置

1. 安装 Mongoose

npm install mongoose

2. 创建配置文件

在项目根目录下创建 config 文件夹,并添加 index.js 配置文件:

// 数据库配置参数
module.exports = {
  DBHOST: '127.0.0.1',     // 数据库主机地址
  DBPORT: 27017,           // 数据库端口
  DBNAME: 'node-ruoyi'     // 数据库名称
}

3. 创建数据库连接模块

db 文件夹下创建 db.js 文件:

// 数据库连接模块
module.exports = function(success, error) {
  // 错误回调函数默认处理
  if (typeof error !== 'function') {
    error = () => {
      console.log('数据库连接失败')
    }
  }
  
  const mongoose = require('mongoose')
  const { DBHOST, DBPORT, DBNAME } = require('../config')
  
  // 启用严格查询模式
  mongoose.set('strictQuery', true)
  
  // 建立数据库连接
  mongoose.connect(`mongodb://${DBHOST}:${DBPORT}/${DBNAME}`)
  
  // 连接成功回调
  mongoose.connection.once('open', () => {
    console.log('MongoDB 连接成功')
    success()
  })
  
  // 连接错误回调
  mongoose.connection.on('error', () => {
    console.log('MongoDB 连接出错')
    error?.()
  })
  
  // 连接关闭回调
  mongoose.connection.on('close', () => {
    console.log('MongoDB 连接已关闭')
  })
}

4. 修改应用入口文件

更新 bin/www 文件,确保数据库连接成功后再启动服务器:

#!/usr/bin/env node

/**
 * 模块依赖
 */
const db = require('../db/db')

// 先建立数据库连接,成功后启动应用
db(() => {
  var app = require('../app')
  var debug = require('debug')('back:server')
  var http = require('http')
  
  /**
   * 从环境变量获取端口号
   */
  var port = normalizePort(process.env.PORT || '3000')
  app.set('port', port)
  
  /**
   * 创建 HTTP 服务器
   */
  var server = http.createServer(app)
  
  /**
   * 监听指定端口
   */
  server.listen(port)
  server.on('error', onError)
  server.on('listening', onListening)
  
  /**
   * 端口号标准化函数
   */
  function normalizePort(val) {
    var port = parseInt(val, 10)
    
    if (isNaN(port)) {
      // 命名管道
      return val
    }
    
    if (port >= 0) {
      // 端口号
      return port
    }
    
    return false
  }
  
  /**
   * HTTP 服务器错误处理
   */
  function onError(error) {
    if (error.syscall !== 'listen') {
      throw error
    }
    
    var bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port
    
    // 处理特定监听错误
    switch (error.code) {
      case 'EACCES':
        console.error(bind + ' 需要提升的权限')
        process.exit(1)
        break
      case 'EADDRINUSE':
        console.error(bind + ' 端口已被占用')
        process.exit(1)
        break
      default:
        throw error
    }
  }
  
  /**
   * HTTP 服务器监听事件
   */
  function onListening() {
    var addr = server.address()
    var bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port
    debug('正在监听 ' + bind)
    console.log(`服务器已启动,监听端口: ${port}`)
  }
})

5. 重启应用服务

npm start

数据库连接成功示意图

总结

通过以上配置,您的 Node.js 应用已经成功集成了 Mongoose 和 MongoDB 数据库。这种配置方式具有以下优势:

  1. 模块化设计:将数据库配置与业务逻辑分离,便于维护
  2. 错误处理完善:包含连接成功、失败和关闭的完整回调机制
  3. 启动顺序优化:确保数据库连接成功后再启动 HTTP 服务
  4. 配置灵活:通过配置文件轻松调整数据库连接参数

现在您的应用已经具备了数据库操作能力,可以开始定义数据模型和进行数据操作了。

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

作者 Lsx_
2026年1月15日 17:17

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 遍历对象顺序完全指南:从混乱到可控

作者 北辰alk
2026年1月15日 16:58

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种跳转方式深度解析

作者 北辰alk
2026年1月15日 16:51

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 的终极区别指南

作者 北辰alk
2026年1月15日 16:43

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机制完全解析

2026年1月15日 16:35

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 代码                                │   │
│ └──────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────┘
❌
❌