阅读视图

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

每日一题-学生分数的最小差值🟢

给你一个 下标从 0 开始 的整数数组 nums ,其中 nums[i] 表示第 i 名学生的分数。另给你一个整数 k

从数组中选出任意 k 名学生的分数,使这 k 个分数间 最高分最低分差值 达到 最小化

返回可能的 最小差值

 

示例 1:

输入:nums = [90], k = 1
输出:0
解释:选出 1 名学生的分数,仅有 1 种方法:
- [90] 最高分和最低分之间的差值是 90 - 90 = 0
可能的最小差值是 0

示例 2:

输入:nums = [9,4,1,7], k = 2
输出:2
解释:选出 2 名学生的分数,有 6 种方法:
- [9,4,1,7] 最高分和最低分之间的差值是 9 - 4 = 5
- [9,4,1,7] 最高分和最低分之间的差值是 9 - 1 = 8
- [9,4,1,7] 最高分和最低分之间的差值是 9 - 7 = 2
- [9,4,1,7] 最高分和最低分之间的差值是 4 - 1 = 3
- [9,4,1,7] 最高分和最低分之间的差值是 7 - 4 = 3
- [9,4,1,7] 最高分和最低分之间的差值是 7 - 1 = 6
可能的最小差值是 2

 

提示:

  • 1 <= k <= nums.length <= 1000
  • 0 <= nums[i] <= 105

【宫水三叶】排序 + 滑动窗口运用题

排序 + 滑动窗口

从 $n$ 个元素里找 $k$ 个,使得 $k$ 个元素最大差值最小。

最大值最小化问题容易想到「二分」,利用答案本身具有「二段性」,来将原本的求解问题转化为判断定问题。

回到本题,容易证明,这 $k$ 个元素必然是有序数组中(排序后)的连续段。反证法,若最佳 $k$ 个选择不是连续段,能够调整为连续段,结果不会变差。

因此我们可以先对 $nums$ 进行排序,然后扫描所有大小为 $k$ 的窗口,直接找到答案,而无须使用「二分」。

代码(二分答案代码见 $P2$):

###Java

class Solution {
    public int minimumDifference(int[] nums, int k) {
        Arrays.sort(nums);
        int n = nums.length, ans = nums[k - 1] - nums[0];
        for (int i = k; i < n; i++) {
            ans = Math.min(ans, nums[i] - nums[i - k + 1]);
        }
        return ans;
    }
}

###Java

class Solution {
    int[] nums; int k;
    public int minimumDifference(int[] _nums, int _k) {
        nums = _nums; k = _k;
        Arrays.sort(nums);
        int l = 0, r = 100010;
        while (l < r) {
            int mid = l + r >> 1;
            if (check(mid)) r = mid;
            else l = mid + 1;
        }
        return r;
    }
    boolean check(int x) {
        int n = nums.length, ans = nums[k - 1] - nums[0];
        for (int i = k; i < n && ans > x; i++) {
            ans = Math.min(ans, nums[i] - nums[i - k + 1]);
        }
        return ans <= x;
    }
}
  • 时间复杂度:排序复杂度为 $O(n\log{n})$;遍历得到答案复杂度为 $O(n)$。整体复杂度为 $O(n\log{n})$
  • 空间复杂度:$O(\log{n})$

其他「滑动窗口」内容

题太简单?来看一道 更贴合笔试/面试的滑动窗口综合题 🎉 🎉

或是加练其他「滑动窗口」内容 🍭🍭🍭

题目 题解 难度 推荐指数
3. 无重复字符的最长子串 LeetCode 题解链接 中等 🤩🤩🤩🤩🤩
30. 串联所有单词的子串 LeetCode 题解链接 困难 🤩🤩
187. 重复的DNA序列 LeetCode 题解链接 中等 🤩🤩🤩🤩
219. 存在重复元素 II LeetCode 题解链接 简单 🤩🤩🤩🤩
424. 替换后的最长重复字符 LeetCode 题解链接 中等 🤩🤩🤩🤩
438. 找到字符串中所有字母异位词 LeetCode 题解链接 中等 🤩🤩🤩🤩
480. 滑动窗口中位数 LeetCode 题解链接 困难 🤩🤩🤩🤩
567. 字符串的排列 LeetCode 题解链接 中等 🤩🤩🤩
594. 最长和谐子序列 LeetCode 题解链接 简单 🤩🤩🤩🤩
643. 子数组最大平均数 I LeetCode 题解链接 简单 🤩🤩🤩🤩🤩
992. K 个不同整数的子数组 LeetCode 题解链接 困难 🤩🤩🤩🤩
1004. 最大连续1的个数 III LeetCode 题解链接 中等 🤩🤩🤩
1052. 爱生气的书店老板 LeetCode 题解链接 中等 🤩🤩🤩
1208. 尽可能使字符串相等 LeetCode 题解链接 中等 🤩🤩🤩
1423. 可获得的最大点数 LeetCode 题解链接 中等 🤩🤩🤩🤩
1438. 绝对差不超过限制的最长连续子数组 LeetCode 题解链接 中等 🤩🤩🤩
1610. 可见点的最大数目 LeetCode 题解链接 困难 🤩🤩🤩🤩
1838. 最高频元素的频数 LeetCode 题解链接 中等 🤩🤩🤩
注:以上目录整理来自 wiki,任何形式的转载引用请保留出处。

最后

如果有帮助到你,请给题解点个赞和收藏,让更多的人看到 ~ ("▔□▔)/

也欢迎你 关注我 和 加入我们的「组队打卡」小群 ,提供写「证明」&「思路」的高质量题解。

所有题解已经加入 刷题指南,欢迎 star 哦 ~

[Python/Java/JavaScript/Go] 排序+双指针滑窗

解题思路

排序后我们要选k个数达到最大最小的差尽可能小,必然是连续的长度为k的子数组的选法,而差值就是最右边的元素减去最左边的元素。
遍历返回其中的最小值即可。

代码

###Python3

class Solution:
    def minimumDifference(self, nums: List[int], k: int) -> int:
        return min(s[i + k - 1] - s[i] for i in range(len(s) - k + 1)) if k > 1 and (s:=sorted(nums)) else 0

###Java

class Solution {
    public int minimumDifference(int[] nums, int k) {
        if(k == 1)
            return 0;
        Arrays.sort(nums);
        int ans = 100005;
        for(int i = 0; i <= nums.length - k; i++)
            ans = Math.min(ans, nums[i + k - 1] - nums[i]);
        return ans;
    }
}

###JavaScript

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number}
 */
var minimumDifference = function(nums, k) {
    if(k == 1)
        return 0
    nums.sort((a,b)=>a-b)
    let ans = 100005
    for(let i = 0; i <= nums.length - k; i++)
        ans = Math.min(ans, nums[i + k - 1] - nums[i])
    return ans
};

###Go

func minimumDifference(nums []int, k int) int {
    if k == 1 {
        return 0
    }
    sort.Ints(nums)
    ans := 100005
    for i := 0; i <= len(nums) - k; i++ {
        ans = min(ans, nums[i + k - 1] - nums[i])
    }
    return ans
}

func min(a, b int) int {
    if a < b {
        return a
    }
    return b
}

学生分数的最小差值

方法一:排序

思路与算法

要想最小化选择的 $k$ 名学生中最高分和最低分的差值,我们一定是在排好序后的数组中连续地进行选择。这是因为在选择时,如果跳过了某个下标 $i$,那么在选择完毕后,将其中的最高分替换成 $\textit{nums}[i]$,最高分一定不会变大,与最低分的差值同样也不会变大。因此,一定存在有一种最优的选择方案,是连续选择了有序数组中的 $k$ 个连续的元素。

这样一来,我们首先对数组 $\textit{nums}$ 进行升序排序,随后使用一个大小固定为 $k$ 的滑动窗口在 $\textit{nums}$ 上进行遍历。记滑动窗口的左边界为 $i$,那么右边界即为 $i+k-1$,窗口中的 $k$ 名学生最高分和最低分的差值即为 $\textit{nums}[i+k-1] - \textit{nums}[i]$。

最终的答案即为所有 $\textit{nums}[i+k-1] - \textit{nums}[i]$ 中的最小值。

代码

###C++

class Solution {
public:
    int minimumDifference(vector<int>& nums, int k) {
        int n = nums.size();
        sort(nums.begin(), nums.end());
        int ans = INT_MAX;
        for (int i = 0; i + k - 1 < n; ++i) {
            ans = min(ans, nums[i + k - 1] - nums[i]);
        }
        return ans;
    }
};

###Java

class Solution {
    public int minimumDifference(int[] nums, int k) {
        int n = nums.length;
        Arrays.sort(nums);
        int ans = Integer.MAX_VALUE;
        for (int i = 0; i + k - 1 < n; ++i) {
            ans = Math.min(ans, nums[i + k - 1] - nums[i]);
        }
        return ans;
    }
}

###C#

public class Solution {
    public int MinimumDifference(int[] nums, int k) {
        int n = nums.Length;
        Array.Sort(nums);
        int ans = int.MaxValue;
        for (int i = 0; i + k - 1 < n; ++i) {
            ans = Math.Min(ans, nums[i + k - 1] - nums[i]);
        }
        return ans;
    }
}

###Python

class Solution:
    def minimumDifference(self, nums: List[int], k: int) -> int:
        nums.sort()
        return min(nums[i + k - 1] - nums[i] for i in range(len(nums) - k + 1))

###C

#define MIN(a, b) ((a) < (b) ? (a) : (b))

int cmp(const void * pa, const void *pb) {
    return *(int *)pa - *(int *)pb;
}

int minimumDifference(int* nums, int numsSize, int k){
    qsort(nums, numsSize, sizeof(int), cmp);
    int ans = INT_MAX;
    for (int i = 0; i + k - 1 < numsSize; ++i) {
        ans = MIN(ans, nums[i + k - 1] - nums[i]);
    }
    return ans;
}

###JavaScript

var minimumDifference = function(nums, k) {
    const n = nums.length;
    nums.sort((a, b) => a - b);
    let ans = Number.MAX_SAFE_INTEGER;
    for (let i = 0; i < n - k + 1; i++) {
        ans = Math.min(ans, nums[i + k - 1] - nums[i]);
    }
    return ans;
};

###go

func minimumDifference(nums []int, k int) int {
    sort.Ints(nums)
    ans := math.MaxInt32
    for i, num := range nums[:len(nums)-k+1] {
        ans = min(ans, nums[i+k-1]-num)
    }
    return ans
}

func min(a, b int) int {
    if a > b {
        return b
    }
    return a
}

复杂度分析

  • 时间复杂度:$O(n \log n)$,其中 $n$ 是数组 $\textit{nums}$ 的长度。排序需要的时间为 $O(n \log n)$,后续遍历需要的时间为 $O(n)$。

  • 空间复杂度:$O(\log n)$,即为排序需要使用的栈空间。

Next.js 页面导航深度解析:Link 组件的全面指南

Next.js 页面导航深度解析:Link 组件的全面指南

一、Next.js 导航系统概述

1.1 客户端导航 vs 服务器端导航

Next.js 提供了两种主要的导航方式:客户端导航和服务器端导航。了解它们的区别是优化应用性能的关键。

// 对比示例
const navigationComparison = {
  客户端导航: {
    特点: [
      '使用 Link 组件或 router.push()',
      '不刷新整个页面',
      '只更新变化的部分',
      '提供更快的用户体验',
      '支持预加载'
    ],
    适用场景: '应用内部页面跳转'
  },
  服务器端导航: {
    特点: [
      '使用传统的 <a> 标签',
      '刷新整个页面',
      '重新加载所有资源',
      '完整的页面重载',
      'SEO友好'
    ],
    适用场景: '外部链接、首次访问'
  }
};

1.2 导航系统架构图

┌─────────────────────────────────────────────┐
│          Next.js 导航系统工作流程            │
├─────────────────────────────────────────────┤
│  1. 用户点击链接                           │
│  2. Next.js 拦截点击事件                   │
│  3. 检查链接是否在应用内部                  │
│  4. 预加载目标页面资源                     │
│  5. 获取页面数据 (getStaticProps等)        │
│  6. 平滑过渡到新页面                       │
│  7. 更新浏览器 URL (不刷新页面)            │
│  8. 滚动位置管理                           │
└─────────────────────────────────────────────┘

二、Link 组件深度解析

2.1 Link 组件基础使用

// 基础链接
import Link from 'next/link';

export default function Navigation() {
  return (
    <nav>
      {/* 基本使用 */}
      <Link href="/">
        <a>首页</a>
      </Link>
      
      {/* Next.js 13+ 语法 */}
      <Link href="/about">
        关于我们
      </Link>
      
      {/* 自定义样式 */}
      <Link href="/products">
        <a className="nav-link">产品</a>
      </Link>
    </nav>
  );
}

2.2 Link 组件完整属性

import Link from 'next/link';

export default function CompleteLinkExample() {
  return (
    <div>
      {/* 1. href - 目标URL */}
      <Link href="/dashboard">
        仪表板
      </Link>
      
      {/* 2. as - URL映射(Next.js 12及之前) */}
      <Link 
        href="/user/[id]/profile" 
        as="/user/123/profile"
      >
        用户资料
      </Link>
      
      {/* 3. prefetch - 预加载控制 */}
      <Link 
        href="/products" 
        prefetch={false}  // 禁用预加载
      >
        产品(不预加载)
      </Link>
      
      {/* 4. replace - 替换当前历史记录 */}
      <Link 
        href="/login" 
        replace  // 替换而不是push
      >
        登录(替换历史)
      </Link>
      
      {/* 5. scroll - 滚动控制 */}
      <Link 
        href="/contact" 
        scroll={false}  // 保持滚动位置
      >
        联系(不滚动到顶部)
      </Link>
      
      {/* 6. shallow - 浅层路由 */}
      <Link 
        href="/?page=2" 
        shallow  // 不运行数据获取方法
      >
        下一页(浅层路由)
      </Link>
      
      {/* 7. locale - 国际化支持 */}
      <Link 
        href="/about" 
        locale="en"  // 切换到英文
      >
        About in English
      </Link>
      
      {/* 8. legacyBehavior - 向后兼容 */}
      <Link 
        href="/old-page" 
        legacyBehavior  // 使用旧版行为
      >
        <a>旧版链接</a>
      </Link>
    </div>
  );
}

三、动态路由导航

3.1 带参数的动态路由

import Link from 'next/link';

export default function DynamicNavigation() {
  const products = [
    { id: 1, name: '笔记本电脑', slug: 'laptop' },
    { id: 2, name: '智能手机', slug: 'smartphone' },
    { id: 3, name: '平板电脑', slug: 'tablet' },
  ];
  
  const categories = [
    { id: 'electronics', name: '电子产品' },
    { id: 'clothing', name: '服装' },
    { id: 'books', name: '图书' },
  ];
  
  return (
    <div>
      <h2>产品导航</h2>
      <ul>
        {products.map(product => (
          <li key={product.id}>
            {/* 字符串模板 */}
            <Link href={`/products/${product.id}`}>
              产品详情 - {product.name}
            </Link>
            
            {/* 对象语法 */}
            <Link href={{
              pathname: '/products/[id]',
              query: { 
                id: product.id,
                name: product.name 
              }
            }}>
              对象语法 - {product.name}
            </Link>
          </li>
        ))}
      </ul>
      
      <h2>嵌套动态路由</h2>
      <ul>
        {categories.map(category => (
          <li key={category.id}>
            <Link href={`/shop/${category.id}/products`}>
              {category.name}
            </Link>
          </li>
        ))}
      </ul>
      
      <h2>多参数路由</h2>
      <Link href="/blog/2024/react-tutorial">
        2024年React教程
      </Link>
    </div>
  );
}

3.2 查询参数导航

import Link from 'next/link';
import { useRouter } from 'next/router';

export default function QueryNavigation() {
  const router = useRouter();
  
  // 当前查询参数
  const currentPage = router.query.page || 1;
  const currentSort = router.query.sort || 'newest';
  const currentCategory = router.query.category || 'all';
  
  // 生成分页链接
  const paginationLinks = [
    { page: 1, label: '第一页' },
    { page: 2, label: '第二页' },
    { page: 3, label: '第三页' },
  ];
  
  // 排序选项
  const sortOptions = [
    { value: 'newest', label: '最新' },
    { value: 'popular', label: '最受欢迎' },
    { value: 'price-low', label: '价格从低到高' },
    { value: 'price-high', label: '价格从高到低' },
  ];
  
  // 分类选项
  const categories = [
    { value: 'all', label: '全部' },
    { value: 'electronics', label: '电子产品' },
    { value: 'books', label: '图书' },
    { value: 'clothing', label: '服装' },
  ];
  
  return (
    <div className="filter-navigation">
      <h2>带查询参数的导航</h2>
      
      {/* 分页导航 */}
      <div className="pagination">
        <h3>分页</h3>
        <div className="pagination-links">
          {paginationLinks.map(({ page, label }) => (
            <Link
              key={page}
              href={{
                pathname: '/products',
                query: { 
                  ...router.query,  // 保持其他查询参数
                  page: page 
                }
              }}
              className={`page-link ${currentPage == page ? 'active' : ''}`}
            >
              {label}
            </Link>
          ))}
        </div>
      </div>
      
      {/* 排序导航 */}
      <div className="sorting">
        <h3>排序方式</h3>
        <div className="sort-options">
          {sortOptions.map(({ value, label }) => (
            <Link
              key={value}
              href={{
                pathname: '/products',
                query: { 
                  ...router.query,
                  sort: value 
                }
              }}
              className={`sort-link ${currentSort === value ? 'active' : ''}`}
            >
              {label}
            </Link>
          ))}
        </div>
      </div>
      
      {/* 分类导航 */}
      <div className="categories">
        <h3>分类筛选</h3>
        <div className="category-links">
          {categories.map(({ value, label }) => (
            <Link
              key={value}
              href={{
                pathname: '/products',
                query: { 
                  ...router.query,
                  category: value 
                }
              }}
              className={`category-link ${currentCategory === value ? 'active' : ''}`}
            >
              {label}
            </Link>
          ))}
        </div>
      </div>
      
      {/* 复杂查询参数示例 */}
      <div className="complex-query">
        <h3>复杂筛选</h3>
        <Link
          href={{
            pathname: '/products',
            query: {
              category: 'electronics',
              minPrice: 1000,
              maxPrice: 5000,
              brand: 'apple,samsung',
              inStock: true,
              sort: 'price-low',
              page: 1
            }
          }}
          className="complex-filter-link"
        >
          查看高端电子产品
        </Link>
      </div>
    </div>
  );
}

四、useRouter 编程式导航

4.1 useRouter 基础使用

import { useRouter } from 'next/router';
import { useState, useEffect } from 'react';

export default function ProgrammaticNavigation() {
  const router = useRouter();
  const [loading, setLoading] = useState(false);
  const [navigationHistory, setNavigationHistory] = useState([]);
  
  // 获取路由信息
  const {
    pathname,      // 当前路径
    query,         // 查询参数对象
    asPath,        // 实际路径(包含查询参数)
    locale,        // 当前语言
    isReady,       // 路由器是否就绪
    isFallback,    // 是否在fallback状态
  } = router;
  
  // 基本导航方法
  const handleNavigation = (path) => {
    // 1. push - 添加新历史记录
    router.push(path);
  };
  
  const handleReplace = (path) => {
    // 2. replace - 替换当前历史记录
    router.replace(path);
  };
  
  const handleBack = () => {
    // 3. back - 返回上一页
    router.back();
  };
  
  const handleForward = () => {
    // 4. forward - 前进
    router.forward();
  };
  
  const handleReload = () => {
    // 5. reload - 重新加载当前页
    router.reload();
  };
  
  // 复杂导航示例
  const navigateWithData = async () => {
    setLoading(true);
    
    try {
      // 模拟API调用
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        body: JSON.stringify({ username: 'user', password: 'pass' })
      });
      
      const data = await response.json();
      
      if (data.success) {
        // 导航到仪表板
        await router.push({
          pathname: '/dashboard',
          query: { 
            welcome: 'true',
            userId: data.userId 
          }
        });
        
        // 添加成功消息
        router.push('/dashboard?message=login-success');
      } else {
        // 显示错误
        router.push('/login?error=invalid-credentials');
      }
    } catch (error) {
      router.push('/login?error=network-error');
    } finally {
      setLoading(false);
    }
  };
  
  // 监听路由变化
  useEffect(() => {
    const handleRouteChangeStart = (url) => {
      console.log('路由开始变化到:', url);
      setLoading(true);
      
      // 记录导航历史
      setNavigationHistory(prev => [...prev, {
        url,
        timestamp: new Date().toISOString(),
        type: 'start'
      }]);
    };
    
    const handleRouteChangeComplete = (url) => {
      console.log('路由变化完成:', url);
      setLoading(false);
      
      setNavigationHistory(prev => [...prev, {
        url,
        timestamp: new Date().toISOString(),
        type: 'complete'
      }]);
    };
    
    const handleRouteChangeError = (err, url) => {
      console.error('路由变化错误:', err);
      setLoading(false);
    };
    
    // 订阅路由事件
    router.events.on('routeChangeStart', handleRouteChangeStart);
    router.events.on('routeChangeComplete', handleRouteChangeComplete);
    router.events.on('routeChangeError', handleRouteChangeError);
    
    return () => {
      router.events.off('routeChangeStart', handleRouteChangeStart);
      router.events.off('routeChangeComplete', handleRouteChangeComplete);
      router.events.off('routeChangeError', handleRouteChangeError);
    };
  }, [router]);
  
  return (
    <div className="programmatic-nav">
      <h2>编程式导航</h2>
      
      {/* 加载指示器 */}
      {loading && (
        <div className="loading-overlay">
          <div className="loading-spinner"></div>
          <p>页面加载中...</p>
        </div>
      )}
      
      {/* 路由信息显示 */}
      <div className="route-info">
        <h3>当前路由信息</h3>
        <pre>
          {JSON.stringify({
            pathname,
            query,
            asPath,
            locale,
            isReady,
            isFallback
          }, null, 2)}
        </pre>
      </div>
      
      {/* 导航控制按钮 */}
      <div className="navigation-controls">
        <button 
          onClick={() => handleNavigation('/about')}
          className="nav-button"
        >
          前往关于页面
        </button>
        
        <button 
          onClick={() => handleReplace('/profile')}
          className="nav-button replace"
        >
          替换到个人资料
        </button>
        
        <button 
          onClick={handleBack}
          className="nav-button back"
        >
          返回
        </button>
        
        <button 
          onClick={handleForward}
          className="nav-button forward"
        >
          前进
        </button>
        
        <button 
          onClick={navigateWithData}
          className="nav-button with-data"
          disabled={loading}
        >
          {loading ? '登录中...' : '登录并导航'}
        </button>
      </div>
      
      {/* 动态参数导航 */}
      <div className="dynamic-navigation">
        <h3>动态生成导航</h3>
        <div className="dynamic-buttons">
          {['home', 'about', 'contact', 'products', 'blog'].map((page) => (
            <button
              key={page}
              onClick={() => router.push(`/${page}`)}
              className={`dynamic-button ${pathname === `/${page}` ? 'active' : ''}`}
            >
              {page.charAt(0).toUpperCase() + page.slice(1)}
            </button>
          ))}
        </div>
      </div>
      
      {/* 导航历史记录 */}
      <div className="navigation-history">
        <h3>导航历史记录</h3>
        <ul>
          {navigationHistory.slice(-5).map((item, index) => (
            <li key={index} className={`history-item ${item.type}`}>
              <span className="timestamp">
                {new Date(item.timestamp).toLocaleTimeString()}
              </span>
              <span className="url">{item.url}</span>
              <span className="type">({item.type})</span>
            </li>
          ))}
        </ul>
      </div>
    </div>
  );
}

4.2 高级路由操作

import { useRouter } from 'next/router';
import { useEffect } from 'react';

export default function AdvancedRouterOperations() {
  const router = useRouter();
  
  // 1. 预取页面
  const prefetchPages = () => {
    // 预取重要页面
    router.prefetch('/dashboard');
    router.prefetch('/profile');
    router.prefetch('/settings');
    
    // 预取动态路由
    router.prefetch('/products/[id]', '/products/123');
    router.prefetch('/blog/[slug]', '/blog/react-tutorial');
  };
  
  // 2. 守卫导航
  const guardedNavigation = async (targetPath) => {
    // 检查是否允许离开当前页
    const allowNavigation = confirm('确定要离开当前页面吗?');
    
    if (allowNavigation) {
      // 保存当前状态
      const currentState = {
        scrollY: window.scrollY,
        formData: getFormData(),
        timestamp: Date.now()
      };
      
      // 保存到sessionStorage
      sessionStorage.setItem(`state:${router.asPath}`, JSON.stringify(currentState));
      
      // 执行导航
      await router.push(targetPath);
    }
  };
  
  // 3. 恢复页面状态
  const restorePageState = () => {
    const savedState = sessionStorage.getItem(`state:${router.asPath}`);
    
    if (savedState) {
      const { scrollY, formData, timestamp } = JSON.parse(savedState);
      
      // 恢复滚动位置
      window.scrollTo(0, scrollY);
      
      // 恢复表单数据
      restoreFormData(formData);
      
      console.log(`恢复 ${new Date(timestamp).toLocaleString()} 的状态`);
    }
  };
  
  // 4. 监听路由变化并恢复状态
  useEffect(() => {
    const handleRouteChange = () => {
      restorePageState();
    };
    
    router.events.on('routeChangeComplete', handleRouteChange);
    
    return () => {
      router.events.off('routeChangeComplete', handleRouteChange);
    };
  }, [router]);
  
  // 5. 自定义导航钩子
  const useNavigationGuard = (shouldBlockNavigation) => {
    useEffect(() => {
      const handleBeforeUnload = (event) => {
        if (shouldBlockNavigation) {
          event.preventDefault();
          event.returnValue = '您有未保存的更改,确定要离开吗?';
        }
      };
      
      const handleRouteChangeStart = (url) => {
        if (shouldBlockNavigation && url !== router.asPath) {
          const confirmed = confirm('您有未保存的更改,确定要离开吗?');
          
          if (!confirmed) {
            // 取消导航
            router.events.emit('routeChangeError');
            throw '取消导航';
          }
        }
      };
      
      // 添加事件监听器
      window.addEventListener('beforeunload', handleBeforeUnload);
      router.events.on('routeChangeStart', handleRouteChangeStart);
      
      return () => {
        window.removeEventListener('beforeunload', handleBeforeUnload);
        router.events.off('routeChangeStart', handleRouteChangeStart);
      };
    }, [shouldBlockNavigation, router]);
  };
  
  // 示例:获取表单数据
  const getFormData = () => {
    // 模拟获取表单数据
    return {
      username: 'john_doe',
      email: 'john@example.com'
    };
  };
  
  // 示例:恢复表单数据
  const restoreFormData = (data) => {
    console.log('恢复表单数据:', data);
    // 实际实现会填充表单字段
  };
  
  return (
    <div className="advanced-router">
      <h2>高级路由操作</h2>
      
      <button onClick={prefetchPages} className="prefetch-button">
        预取重要页面
      </button>
      
      <button 
        onClick={() => guardedNavigation('/new-page')}
        className="guarded-nav-button"
      >
        带确认的导航
      </button>
      
      <button onClick={restorePageState} className="restore-button">
        恢复页面状态
      </button>
    </div>
  );
}

五、导航性能优化

5.1 智能预加载策略

import Link from 'next/link';
import { useEffect, useState } from 'react';

export default function SmartPrefetchNavigation() {
  const [visibleLinks, setVisibleLinks] = useState([]);
  const [hoveredLink, setHoveredLink] = useState(null);
  const [connectionType, setConnectionType] = useState('4g');
  
  // 检测网络连接
  useEffect(() => {
    if ('connection' in navigator) {
      const connection = navigator.connection;
      setConnectionType(connection.effectiveType);
      
      const updateConnection = () => {
        setConnectionType(connection.effectiveType);
      };
      
      connection.addEventListener('change', updateConnection);
      return () => connection.removeEventListener('change', updateConnection);
    }
  }, []);
  
  // 智能预加载策略
  const getPrefetchStrategy = (link) => {
    // 根据网络状况调整预加载策略
    const strategies = {
      'slow-2g': 'none',        // 慢速网络不预加载
      '2g': 'hover-only',       // 2G网络只在悬停时预加载
      '3g': 'viewport',         // 3G网络预加载视口内链接
      '4g': 'aggressive',       // 4G网络积极预加载
      '5g': 'all'              // 5G网络预加载所有链接
    };
    
    return strategies[connectionType] || 'viewport';
  };
  
  // 视口检测
  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        const visible = entries
          .filter(entry => entry.isIntersecting)
          .map(entry => entry.target.dataset.link);
        
        setVisibleLinks(visible);
      },
      {
        rootMargin: '50px', // 提前50px检测
        threshold: 0.1
      }
    );
    
    // 观察所有链接
    document.querySelectorAll('[data-link]').forEach(el => {
      observer.observe(el);
    });
    
    return () => observer.disconnect();
  }, []);
  
  // 页面重要性评分
  const pageImportance = {
    '/': 10,           // 首页最重要
    '/products': 8,    // 产品页重要
    '/about': 6,       // 关于页中等重要
    '/contact': 5,     // 联系页
    '/blog': 7,        // 博客页
  };
  
  const shouldPrefetch = (href) => {
    const strategy = getPrefetchStrategy(href);
    const importance = pageImportance[href] || 3;
    
    switch (strategy) {
      case 'all':
        return true;
      case 'aggressive':
        return importance >= 5;
      case 'viewport':
        return visibleLinks.includes(href);
      case 'hover-only':
        return hoveredLink === href;
      case 'none':
      default:
        return false;
    }
  };
  
  const navigationItems = [
    { href: '/', label: '首页', importance: '高' },
    { href: '/products', label: '产品', importance: '高' },
    { href: '/about', label: '关于', importance: '中' },
    { href: '/contact', label: '联系', importance: '中' },
    { href: '/blog', label: '博客', importance: '高' },
    { href: '/faq', label: 'FAQ', importance: '低' },
  ];
  
  return (
    <div className="smart-navigation">
      <h2>智能预加载导航</h2>
      
      <div className="network-status">
        <p>当前网络: <strong>{connectionType}</strong></p>
        <p>预加载策略: <strong>{getPrefetchStrategy()}</strong></p>
      </div>
      
      <nav className="smart-nav">
        {navigationItems.map(({ href, label, importance }) => (
          <div
            key={href}
            data-link={href}
            className="nav-item-container"
            onMouseEnter={() => setHoveredLink(href)}
            onMouseLeave={() => setHoveredLink(null)}
          >
            <Link
              href={href}
              prefetch={shouldPrefetch(href)}
              className={`nav-link importance-${importance.toLowerCase()}`}
            >
              {label}
              <span className="prefetch-indicator">
                {shouldPrefetch(href) ? '✓ 预加载' : '✗ 不预加载'}
              </span>
            </Link>
          </div>
        ))}
      </nav>
      
      {/* 预加载状态显示 */}
      <div className="prefetch-status">
        <h3>预加载状态</h3>
        <ul>
          {navigationItems.map(({ href, label }) => (
            <li key={href}>
              {label}: {shouldPrefetch(href) ? '正在预加载' : '等待触发'}
            </li>
          ))}
        </ul>
      </div>
    </div>
  );
}

5.2 导航缓存策略

import { useState, useEffect, useCallback } from 'react';
import { useRouter } from 'next/router';
import Link from 'next/link';

export default function NavigationCacheStrategy() {
  const router = useRouter();
  const [cache, setCache] = useState(new Map());
  const [navigationStats, setNavigationStats] = useState({
    hits: 0,
    misses: 0,
    size: 0
  });
  
  // 缓存页面状态
  const cachePageState = useCallback((url, state) => {
    setCache(prev => {
      const newCache = new Map(prev);
      
      // 限制缓存大小
      if (newCache.size >= 10) {
        const firstKey = newCache.keys().next().value;
        newCache.delete(firstKey);
      }
      
      newCache.set(url, {
        ...state,
        timestamp: Date.now(),
        expiry: Date.now() + (5 * 60 * 1000) // 5分钟过期
      });
      
      return newCache;
    });
  }, []);
  
  // 获取缓存的页面状态
  const getCachedPageState = useCallback((url) => {
    const cached = cache.get(url);
    
    if (cached && cached.expiry > Date.now()) {
      setNavigationStats(prev => ({
        ...prev,
        hits: prev.hits + 1
      }));
      return cached;
    }
    
    setNavigationStats(prev => ({
      ...prev,
      misses: prev.misses + 1
    }));
    
    return null;
  }, [cache]);
  
  // 监听路由变化
  useEffect(() => {
    const handleRouteChangeStart = (url) => {
      // 保存当前页面状态
      const currentState = {
        scrollY: window.scrollY,
        formData: collectFormData(),
        componentState: collectComponentState()
      };
      
      cachePageState(router.asPath, currentState);
    };
    
    const handleRouteChangeComplete = (url) => {
      // 尝试恢复缓存的状态
      const cachedState = getCachedPageState(url);
      
      if (cachedState) {
        // 恢复滚动位置
        requestAnimationFrame(() => {
          window.scrollTo(0, cachedState.scrollY);
        });
        
        // 恢复表单数据
        restoreFormData(cachedState.formData);
        
        // 恢复组件状态
        restoreComponentState(cachedState.componentState);
        
        console.log('从缓存恢复页面状态');
      }
    };
    
    router.events.on('routeChangeStart', handleRouteChangeStart);
    router.events.on('routeChangeComplete', handleRouteChangeComplete);
    
    return () => {
      router.events.off('routeChangeStart', handleRouteChangeStart);
      router.events.off('routeChangeComplete', handleRouteChangeComplete);
    };
  }, [router, cachePageState, getCachedPageState]);
  
  // 收集表单数据(示例)
  const collectFormData = () => {
    // 实际实现中收集所有表单数据
    return {
      search: document.querySelector('input[type="search"]')?.value || '',
      filters: {}
    };
  };
  
  // 收集组件状态(示例)
  const collectComponentState = () => {
    return {
      activeTab: 'description',
      expandedItems: [1, 3],
      sortOrder: 'asc'
    };
  };
  
  // 恢复表单数据(示例)
  const restoreFormData = (formData) => {
    // 实际实现中恢复表单数据
    console.log('恢复表单数据:', formData);
  };
  
  // 恢复组件状态(示例)
  const restoreComponentState = (componentState) => {
    console.log('恢复组件状态:', componentState);
  };
  
  return (
    <div className="cache-strategy">
      <h2>导航缓存策略</h2>
      
      <div className="cache-stats">
        <h3>缓存统计</h3>
        <div className="stats-grid">
          <div className="stat-item">
            <span className="stat-label">缓存命中</span>
            <span className="stat-value">{navigationStats.hits}</span>
          </div>
          <div className="stat-item">
            <span className="stat-label">缓存未命中</span>
            <span className="stat-value">{navigationStats.misses}</span>
          </div>
          <div className="stat-item">
            <span className="stat-label">命中率</span>
            <span className="stat-value">
              {navigationStats.hits + navigationStats.misses > 0
                ? `${((navigationStats.hits / (navigationStats.hits + navigationStats.misses)) * 100).toFixed(1)}%`
                : '0%'
              }
            </span>
          </div>
          <div className="stat-item">
            <span className="stat-label">缓存大小</span>
            <span className="stat-value">{cache.size} 页</span>
          </div>
        </div>
      </div>
      
      <div className="cached-pages">
        <h3>已缓存的页面</h3>
        <ul>
          {Array.from(cache.entries()).map(([url, data]) => (
            <li key={url}>
              <Link href={url}>
                {url} 
                <span className="cache-age">
                  ({Math.round((Date.now() - data.timestamp) / 1000)}秒前)
                </span>
              </Link>
            </li>
          ))}
        </ul>
      </div>
      
      {/* 测试链接 */}
      <div className="test-links">
        <h3>测试缓存效果</h3>
        <div className="link-group">
          <Link href="/page1">页面1</Link>
          <Link href="/page2">页面2</Link>
          <Link href="/page3">页面3</Link>
          <Link href="/page4">页面4</Link>
        </div>
      </div>
    </div>
  );
}

六、导航动画与过渡效果

6.1 页面过渡动画

import Link from 'next/link';
import { useRouter } from 'next/router';
import { motion, AnimatePresence } from 'framer-motion';
import { useState } from 'react';

export default function AnimatedNavigation() {
  const router = useRouter();
  const [isAnimating, setIsAnimating] = useState(false);
  
  // 自定义导航处理
  const handleAnimatedNavigation = async (href) => {
    setIsAnimating(true);
    
    // 等待动画完成
    await new Promise(resolve => setTimeout(resolve, 300));
    
    // 执行导航
    await router.push(href);
    
    setIsAnimating(false);
  };
  
  // 页面过渡动画配置
  const pageVariants = {
    initial: {
      opacity: 0,
      x: -100,
      scale: 0.95
    },
    enter: {
      opacity: 1,
      x: 0,
      scale: 1,
      transition: {
        duration: 0.5,
        ease: [0.43, 0.13, 0.23, 0.96]
      }
    },
    exit: {
      opacity: 0,
      x: 100,
      scale: 0.95,
      transition: {
        duration: 0.3,
        ease: [0.43, 0.13, 0.23, 0.96]
      }
    }
  };
  
  // 链接悬停动画
  const linkVariants = {
    rest: { 
      scale: 1,
      color: "#666"
    },
    hover: { 
      scale: 1.05,
      color: "#0070f3",
      transition: {
        duration: 0.2,
        type: "spring",
        stiffness: 400,
        damping: 17
      }
    },
    tap: { 
      scale: 0.95 
    }
  };
  
  return (
    <div className="animated-navigation">
      {/* 加载动画遮罩 */}
      <AnimatePresence>
        {isAnimating && (
          <motion.div
            className="navigation-overlay"
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
          >
            <motion.div
              className="loading-spinner"
              animate={{ rotate: 360 }}
              transition={{
                duration: 1,
                repeat: Infinity,
                ease: "linear"
              }}
            />
            <p>加载中...</p>
          </motion.div>
        )}
      </AnimatePresence>
      
      <nav className="animated-nav">
        <motion.ul
          className="nav-list"
          initial="hidden"
          animate="visible"
          variants={{
            hidden: { opacity: 0 },
            visible: {
              opacity: 1,
              transition: {
                staggerChildren: 0.1
              }
            }
          }}
        >
          {['首页', '产品', '关于', '博客', '联系'].map((item, index) => {
            const href = item === '首页' ? '/' : `/${item}`;
            
            return (
              <motion.li
                key={item}
                className="nav-item"
                variants={{
                  hidden: { y: -20, opacity: 0 },
                  visible: {
                    y: 0,
                    opacity: 1,
                    transition: {
                      type: "spring",
                      stiffness: 100
                    }
                  }
                }}
                whileHover="hover"
                whileTap="tap"
              >
                <motion.div
                  variants={linkVariants}
                  className="nav-link-wrapper"
                >
                  <Link 
                    href={href}
                    onClick={(e) => {
                      e.preventDefault();
                      handleAnimatedNavigation(href);
                    }}
                    className={`nav-link ${router.pathname === href ? 'active' : ''}`}
                  >
                    {item}
                    
                    {/* 活动指示器 */}
                    {router.pathname === href && (
                      <motion.div
                        className="active-indicator"
                        layoutId="activeIndicator"
                        initial={false}
                        transition={{
                          type: "spring",
                          stiffness: 380,
                          damping: 30
                        }}
                      />
                    )}
                  </Link>
                </motion.div>
              </motion.li>
            );
          })}
        </motion.ul>
      </nav>
      
      {/* 页面内容区域 */}
      <AnimatePresence mode="wait">
        <motion.div
          key={router.pathname}
          initial="initial"
          animate="enter"
          exit="exit"
          variants={pageVariants}
          className="page-content"
        >
          <h2>当前页面: {router.pathname === '/' ? '首页' : router.pathname.slice(1)}</h2>
          
          {/* 面包屑导航 */}
          <motion.div 
            className="breadcrumb"
            initial={{ opacity: 0, y: 10 }}
            animate={{ opacity: 1, y: 0 }}
            transition={{ delay: 0.1 }}
          >
            <Link href="/">首页</Link>
            {router.pathname !== '/' && (
              <>
                <span> / </span>
                <span>{router.pathname.slice(1)}</span>
              </>
            )}
          </motion.div>
          
          {/* 页面内容 */}
          <motion.div 
            className="content"
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            transition={{ delay: 0.2 }}
          >
            <p>这是 {router.pathname === '/' ? '首页' : router.pathname.slice(1)} 的内容。</p>
            
            {/* 示例卡片 */}
            <div className="card-grid">
              {[1, 2, 3, 4].map((card) => (
                <motion.div
                  key={card}
                  className="card"
                  initial={{ opacity: 0, scale: 0.8 }}
                  animate={{ opacity: 1, scale: 1 }}
                  transition={{ delay: 0.1 * card }}
                  whileHover={{ 
                    scale: 1.05,
                    boxShadow: "0 10px 30px rgba(0, 0, 0, 0.1)"
                  }}
                >
                  <h3>卡片 {card}</h3>
                  <p>这是卡片内容 {card}</p>
                </motion.div>
              ))}
            </div>
          </motion.div>
        </motion.div>
      </AnimatePresence>
    </div>
  );
}

七、移动端导航优化

import { useState, useEffect } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { 
  Home, 
  ShoppingBag, 
  Info, 
  Book, 
  Phone,
  Menu,
  X,
  ChevronLeft,
  ChevronRight
} from 'lucide-react';

export default function MobileOptimizedNavigation() {
  const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
  const [touchStart, setTouchStart] = useState(null);
  const [touchEnd, setTouchEnd] = useState(null);
  const [showBottomNav, setShowBottomNav] = useState(true);
  const [lastScrollY, setLastScrollY] = useState(0);
  const router = useRouter();
  
  // 移动端检测
  const [isMobile, setIsMobile] = useState(false);
  
  useEffect(() => {
    const checkMobile = () => {
      setIsMobile(window.innerWidth <= 768);
    };
    
    checkMobile();
    window.addEventListener('resize', checkMobile);
    
    return () => window.removeEventListener('resize', checkMobile);
  }, []);
  
  // 滑动检测
  useEffect(() => {
    const handleTouchStart = (e) => {
      setTouchStart(e.touches[0].clientX);
    };
    
    const handleTouchMove = (e) => {
      setTouchEnd(e.touches[0].clientX);
    };
    
    const handleTouchEnd = () => {
      if (!touchStart || !touchEnd) return;
      
      const distance = touchStart - touchEnd;
      const isLeftSwipe = distance > 50;
      const isRightSwipe = distance < -50;
      
      if (isLeftSwipe) {
        // 左滑 - 前进(如果历史记录存在)
        router.forward();
      } else if (isRightSwipe) {
        // 右滑 - 后退
        router.back();
      }
      
      setTouchStart(null);
      setTouchEnd(null);
    };
    
    // 监听滚动隐藏/显示底部导航
    const handleScroll = () => {
      const currentScrollY = window.scrollY;
      
      if (currentScrollY > lastScrollY && currentScrollY > 100) {
        // 向下滚动,隐藏底部导航
        setShowBottomNav(false);
      } else if (currentScrollY < lastScrollY || currentScrollY <= 100) {
        // 向上滚动或接近顶部,显示底部导航
        setShowBottomNav(true);
      }
      
      setLastScrollY(currentScrollY);
    };
    
    if (isMobile) {
      window.addEventListener('touchstart', handleTouchStart);
      window.addEventListener('touchmove', handleTouchMove);
      window.addEventListener('touchend', handleTouchEnd);
      window.addEventListener('scroll', handleScroll, { passive: true });
    }
    
    return () => {
      if (isMobile) {
        window.removeEventListener('touchstart', handleTouchStart);
        window.removeEventListener('touchmove', handleTouchMove);
        window.removeEventListener('touchend', handleTouchEnd);
        window.removeEventListener('scroll', handleScroll);
      }
    };
  }, [isMobile, touchStart, touchEnd, lastScrollY, router]);
  
  // 导航项配置
  const navItems = [
    { href: '/', label: '首页', icon: Home, mobileOnly: false },
    { href: '/products', label: '产品', icon: ShoppingBag, mobileOnly: false },
    { href: '/about', label: '关于', icon: Info, mobileOnly: false },
    { href: '/blog', label: '博客', icon: Book, mobileOnly: true },
    { href: '/contact', label: '联系', icon: Phone, mobileOnly: true },
  ];
  
  // 获取当前页面的图标
  const getCurrentPageIcon = () => {
    const currentItem = navItems.find(item => 
      item.href === router.pathname || 
      (item.href === '/' && router.pathname === '/')
    );
    return currentItem ? currentItem.icon : Home;
  };
  
  const CurrentIcon = getCurrentPageIcon();
  
  return (
    <div className="mobile-optimized-nav">
      {/* 顶部导航栏(移动端) */}
      {isMobile && (
        <header className="mobile-header">
          <button 
            className="menu-toggle"
            onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
            aria-label="菜单"
          >
            {isMobileMenuOpen ? <X size={24} /> : <Menu size={24} />}
          </button>
          
          <div className="mobile-header-title">
            <CurrentIcon size={20} />
            <span className="page-title">
              {navItems.find(item => 
                item.href === router.pathname || 
                (item.href === '/' && router.pathname === '/')
              )?.label || '页面'}
            </span>
          </div>
          
          {/* 滑动指示器 */}
          <div className="swipe-indicator">
            <ChevronLeft size={16} />
            <span>滑动返回</span>
            <ChevronRight size={16} />
          </div>
        </header>
      )}
      
      {/* 移动端侧滑菜单 */}
      {isMobile && isMobileMenuOpen && (
        <div className="mobile-menu-overlay">
          <div className="mobile-menu">
            <div className="mobile-menu-header">
              <h3>导航菜单</h3>
              <button 
                onClick={() => setIsMobileMenuOpen(false)}
                className="close-menu"
              >
                <X size={24} />
              </button>
            </div>
            
            <nav className="mobile-menu-nav">
              {navItems.map(({ href, label, icon: Icon }) => (
                <Link
                  key={href}
                  href={href}
                  onClick={() => setIsMobileMenuOpen(false)}
                  className={`mobile-menu-item ${router.pathname === href ? 'active' : ''}`}
                >
                  <Icon size={20} />
                  <span>{label}</span>
                  {router.pathname === href && (
                    <span className="active-dot"></span>
                  )}
                </Link>
              ))}
            </nav>
            
            <div className="mobile-menu-footer">
              <div className="user-info">
                <div className="user-avatar">U</div>
                <div className="user-details">
                  <p className="user-name">访客用户</p>
                  <p className="user-status">未登录</p>
                </div>
              </div>
              
              <div className="quick-actions">
                <button className="quick-action">
                  <span>设置</span>
                </button>
                <button className="quick-action">
                  <span>帮助</span>
                </button>
              </div>
            </div>
          </div>
        </div>
      )}
      
      {/* 桌面端导航 */}
      {!isMobile && (
        <nav className="desktop-nav">
          <div className="desktop-nav-inner">
            <div className="nav-logo">
              <Link href="/">我的网站</Link>
            </div>
            
            <div className="desktop-nav-items">
              {navItems
                .filter(item => !item.mobileOnly)
                .map(({ href, label }) => (
                  <Link
                    key={href}
                    href={href}
                    className={`desktop-nav-item ${router.pathname === href ? 'active' : ''}`}
                  >
                    {label}
                  </Link>
                ))}
            </div>
            
            <div className="desktop-nav-actions">
              <button className="nav-action-button">登录</button>
              <button className="nav-action-button primary">注册</button>
            </div>
          </div>
        </nav>
      )}
      
      {/* 移动端底部导航 */}
      {isMobile && (
        <div className={`mobile-bottom-nav ${showBottomNav ? 'visible' : 'hidden'}`}>
          {navItems
            .filter(item => !item.mobileOnly)
            .map(({ href, label, icon: Icon }) => (
              <Link
                key={href}
                href={href}
                className={`bottom-nav-item ${router.pathname === href ? 'active' : ''}`}
              >
                <Icon size={22} />
                <span className="bottom-nav-label">{label}</span>
              </Link>
            ))}
        </div>
      )}
      
      {/* 主内容区域 */}
      <main className={`main-content ${isMobile ? 'mobile' : 'desktop'}`}>
        <div className="content-wrapper">
          <h1>响应式导航演示</h1>
          <p>当前设备: {isMobile ? '移动端' : '桌面端'}</p>
          
          <div className="demo-section">
            <h2>导航特性演示</h2>
            
            <div className="feature-grid">
              <div className="feature-card">
                <h3>滑动导航</h3>
                <p>在移动端尝试左右滑动来前进/后退</p>
              </div>
              
              <div className="feature-card">
                <h3>智能隐藏</h3>
                <p>向下滚动时自动隐藏底部导航</p>
              </div>
              
              <div className="feature-card">
                <h3>触控优化</h3>
                <p>大触摸目标和触觉反馈</p>
              </div>
              
              <div className="feature-card">
                <h3>性能优化</h3>
                <p>移动端特有的性能优化策略</p>
              </div>
            </div>
          </div>
          
          <div className="navigation-test">
            <h2>导航测试</h2>
            <div className="test-buttons">
              <button 
                onClick={() => router.back()}
                className="test-button"
              >
                返回上一页
              </button>
              
              <button 
                onClick={() => router.forward()}
                className="test-button"
              >
                前进
              </button>
              
              <button 
                onClick={() => router.reload()}
                className="test-button"
              >
                重新加载
              </button>
            </div>
          </div>
        </div>
      </main>
    </div>
  );
}

八、导航错误处理与调试

import { useState, useEffect } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/router';

export default function NavigationDebugger() {
  const router = useRouter();
  const [errors, setErrors] = useState([]);
  const [performanceMetrics, setPerformanceMetrics] = useState([]);
  const [debugMode, setDebugMode] = useState(false);
  
  // 导航错误处理
  useEffect(() => {
    const handleRouteError = (err, url) => {
      const error = {
        type: 'ROUTE_ERROR',
        url,
        error: err.toString(),
        timestamp: new Date().toISOString(),
        userAgent: navigator.userAgent
      };
      
      setErrors(prev => [error, ...prev.slice(0, 9)]); // 保留最近10个错误
      
      // 发送错误到监控服务
      logErrorToService(error);
      
      // 用户友好的错误处理
      if (err.cancelled) {
        console.log('导航被取消');
      } else {
        console.error('导航错误:', err);
        alert(`无法加载页面: ${url}\n错误: ${err.message}`);
      }
    };
    
    // 性能监控
    const handleRouteChangeStart = (url) => {
      const startTime = performance.now();
      const startMemory = performance.memory?.usedJSHeapSize;
      
      const metric = {
        url,
        startTime,
        startMemory,
        status: 'loading'
      };
      
      setPerformanceMetrics(prev => {
        const updated = [metric, ...prev.slice(0, 9)];
        return updated;
      });
    };
    
    const handleRouteChangeComplete = (url) => {
      const endTime = performance.now();
      const endMemory = performance.memory?.usedJSHeapSize;
      
      setPerformanceMetrics(prev => {
        if (prev.length === 0) return prev;
        
        const lastMetric = prev[0];
        const duration = endTime - lastMetric.startTime;
        const memoryDiff = endMemory && lastMetric.startMemory 
          ? endMemory - lastMetric.startMemory 
          : null;
        
        return [{
          ...lastMetric,
          endTime,
          endMemory,
          duration,
          memoryDiff,
          status: 'complete'
        }, ...prev.slice(1)];
      });
    };
    
    // 订阅事件
    router.events.on('routeChangeError', handleRouteError);
    router.events.on('routeChangeStart', handleRouteChangeStart);
    router.events.on('routeChangeComplete', handleRouteChangeComplete);
    
    return () => {
      router.events.off('routeChangeError', handleRouteError);
      router.events.off('routeChangeStart', handleRouteChangeStart);
      router.events.off('routeChangeComplete', handleRouteChangeComplete);
    };
  }, [router]);
  
  // 模拟错误发送到监控服务
  const logErrorToService = (error) => {
    // 实际项目中会发送到Sentry、LogRocket等服务
    console.log('发送错误到监控服务:', error);
  };
  
  // 测试错误导航
  const testErrorNavigation = () => {
    // 故意导航到不存在的页面
    router.push('/non-existent-page-12345');
  };
  
  // 测试慢速导航
  const testSlowNavigation = async () => {
    // 模拟慢速加载
    router.push('/slow-page');
  };
  
  // 清理错误日志
  const clearErrors = () => {
    setErrors([]);
  };
  
  // 获取性能评分
  const getPerformanceScore = (duration) => {
    if (duration < 100) return { score: '优秀', color: 'green' };
    if (duration < 300) return { score: '良好', color: 'blue' };
    if (duration < 500) return { score: '一般', color: 'yellow' };
    return { score: '较差', color: 'red' };
  };
  
  return (
    <div className="navigation-debugger">
      <div className="debugger-header">
        <h2>导航调试器</h2>
        <div className="debug-controls">
          <button 
            onClick={() => setDebugMode(!debugMode)}
            className={`debug-toggle ${debugMode ? 'active' : ''}`}
          >
            {debugMode ? '关闭调试' : '开启调试'}
          </button>
          <button onClick={clearErrors} className="clear-button">
            清理错误
          </button>
        </div>
      </div>
      
      {debugMode && (
        <div className="debug-panels">
          {/* 错误面板 */}
          <div className="debug-panel error-panel">
            <h3>导航错误日志</h3>
            <div className="error-controls">
              <button onClick={testErrorNavigation} className="test-error-button">
                测试错误导航
              </button>
              <button onClick={testSlowNavigation} className="test-slow-button">
                测试慢速导航
              </button>
            </div>
            
            {errors.length === 0 ? (
              <p className="no-errors">暂无错误</p>
            ) : (
              <div className="error-list">
                {errors.map((error, index) => (
                  <div key={index} className="error-item">
                    <div className="error-header">
                      <span className="error-type">{error.type}</span>
                      <span className="error-time">
                        {new Date(error.timestamp).toLocaleTimeString()}
                      </span>
                    </div>
                    <div className="error-url">URL: {error.url}</div>
                    <div className="error-message">错误: {error.error}</div>
                  </div>
                ))}
              </div>
            )}
          </div>
          
          {/* 性能面板 */}
          <div className="debug-panel performance-panel">
            <h3>导航性能监控</h3>
            
            {performanceMetrics.length === 0 ? (
              <p className="no-metrics">暂无性能数据</p>
            ) : (
              <div className="performance-metrics">
                {performanceMetrics
                  .filter(metric => metric.status === 'complete')
                  .map((metric, index) => {
                    const score = getPerformanceScore(metric.duration);
                    
                    return (
                      <div key={index} className="metric-item">
                        <div className="metric-header">
                          <span className="metric-url">{metric.url}</span>
                          <span className={`metric-score ${score.color}`}>
                            {score.score} ({metric.duration.toFixed(1)}ms)
                          </span>
                        </div>
                        <div className="metric-details">
                          <div className="metric-detail">
                            <span>持续时间:</span>
                            <span>{metric.duration.toFixed(1)}ms</span>
                          </div>
                          {metric.memoryDiff && (
                            <div className="metric-detail">
                              <span>内存变化:</span>
                              <span>
                                {(metric.memoryDiff / 1024 / 1024).toFixed(2)} MB
                              </span>
                            </div>
                          )}
                        </div>
                      </div>
                    );
                  })}
              </div>
            )}
            
            <div className="performance-summary">
              <h4>性能基准</h4>
              <ul>
                <li>优秀: &lt; 100ms</li>
                <li>良好: 100-300ms</li>
                <li>一般: 300-500ms</li>
                <li>较差: &gt; 500ms</li>
              </ul>
            </div>
          </div>
          
          {/* 路由信息面板 */}
          <div className="debug-panel route-info-panel">
            <h3>当前路由信息</h3>
            <div className="route-info">
              <div className="info-item">
                <span className="info-label">路径名:</span>
                <span className="info-value">{router.pathname}</span>
              </div>
              <div className="info-item">
                <span className="info-label">查询参数:</span>
                <span className="info-value">
                  {JSON.stringify(router.query)}
                </span>
              </div>
              <div className="info-item">
                <span className="info-label">实际路径:</span>
                <span className="info-value">{router.asPath}</span>
              </div>
              <div className="info-item">
                <span className="info-label">语言:</span>
                <span className="info-value">{router.locale}</span>
              </div>
              <div className="info-item">
                <span className="info-label">路由器就绪:</span>
                <span className={`info-value ${router.isReady ? 'ready' : 'not-ready'}`}>
                  {router.isReady ? '是' : '否'}
                </span>
              </div>
              <div className="info-item">
                <span className="info-label">Fallback状态:</span>
                <span className={`info-value ${router.isFallback ? 'fallback' : 'normal'}`}>
                  {router.isFallback ? '是' : '否'}
                </span>
              </div>
            </div>
          </div>
        </div>
      )}
      
      {/* 导航测试链接 */}
      <div className="navigation-test-section">
        <h3>导航测试</h3>
        <div className="test-links">
          <Link href="/" className="test-link">
            首页
          </Link>
          <Link href="/about" className="test-link">
            关于页面
          </Link>
          <Link 
            href={{
              pathname: '/user/[id]',
              query: { id: '123' }
            }}
            className="test-link"
          >
            用户页面
          </Link>
          <Link 
            href="/search?q=test&sort=recent"
            className="test-link"
          >
            搜索页面
          </Link>
          <button 
            onClick={() => router.push('/dynamic/' + Date.now())}
            className="test-link"
          >
            动态页面
          </button>
        </div>
      </div>
    </div>
  );
}

九、Link 组件最佳实践总结

9.1 性能优化实践

// 最佳实践示例
const linkBestPractices = {
  1: {
    实践: '智能预加载',
    代码示例: `
      // 只在需要时预加载
      <Link 
        href="/dashboard" 
        prefetch={shouldPrefetch('/dashboard')}
      >
        仪表板
      </Link>
    `,
    说明: '根据页面重要性和用户行为决定是否预加载'
  },
  
  2: {
    实践: '优先使用客户端导航',
    代码示例: `
      // 使用Link组件而不是<a>标签
      <Link href="/products">
        <a>产品</a>
      </Link>
    `,
    说明: '提供更快的页面切换体验'
  },
  
  3: {
    实践: '正确处理动态路由',
    代码示例: `
      // 使用对象语法
      <Link href={{
        pathname: '/products/[id]',
        query: { id: product.id }
      }}>
        {product.name}
      </Link>
    `,
    说明: '避免字符串拼接错误'
  },
  
  4: {
    实践: '实现渐进增强',
    代码示例: `
      // 为不支持JavaScript的客户端提供回退
      <Link href="/about">
        <a className="no-js-fallback">
          关于我们
        </a>
      </Link>
    `,
    说明: '确保所有用户都能正常访问'
  },
  
  5: {
    实践: '监控导航性能',
    代码示例: `
      // 使用路由事件监听
      router.events.on('routeChangeComplete', (url) => {
        // 发送性能指标
        trackNavigationPerformance(url);
      });
    `,
    说明: '持续优化导航体验'
  }
};

9.2 常见问题与解决方案

const commonProblemsAndSolutions = {
  问题1: 'Link组件不工作',
  解决方案: [
    '检查href属性是否正确',
    '确保在Next.js项目中使用',
    '验证页面文件是否存在',
    '检查是否有语法错误'
  ],
  
  问题2: '预加载太多页面',
  解决方案: [
    '使用prefetch={false}禁用不必要的预加载',
    '根据网络状况调整策略',
    '只预加载重要页面',
    '实现懒加载策略'
  ],
  
  问题3: '导航状态管理困难',
  解决方案: [
    '使用路由事件监听器',
    '实现导航守卫',
    '保存和恢复页面状态',
    '使用状态管理库'
  ],
  
  问题4: '移动端体验不佳',
  解决方案: [
    '实现响应式导航',
    '添加触控反馈',
    '优化加载状态',
    '使用骨架屏'
  ],
  
  问题5: 'SEO问题',
  解决方案: [
    '确保重要链接使用<a>标签',
    '实现合理的链接结构',
    '使用语义化HTML',
    '添加结构化数据'
  ]
};

十、总结

Next.js 的导航系统提供了强大而灵活的功能,核心要点包括:

关键特性:

  1. Link 组件:实现客户端导航,支持预加载、滚动控制等高级功能
  2. useRouter 钩子:提供编程式导航和路由信息访问
  3. 智能预加载:自动优化页面加载性能
  4. 平滑过渡:支持页面切换动画
  5. 错误处理:完善的导航错误处理和恢复机制

最佳实践:

  1. 根据场景选择合适的导航方式
  2. 实现智能预加载策略
  3. 优化移动端导航体验
  4. 监控导航性能
  5. 处理边缘情况和错误

性能优化:

  1. 合理使用预加载
  2. 实现导航缓存
  3. 优化首次加载
  4. 减少不必要的重渲染

通过深入理解和合理应用 Next.js 的导航功能,可以构建出高性能、用户体验优秀的现代 Web 应用。无论是简单的网站还是复杂的企业级应用,Next.js 都能提供强大的导航解决方案。

1.8GB 内存也能跑大模型!Ollama Docker 部署完整指南

想在服务器上部署私有 AI 模型,但内存不够用?本文教你用 Docker + Swap 优化,让低配服务器也能流畅运行 Ollama 大模型。

背景

为什么选择 Docker 部署?

因为直接使用命令会报错,无法运行ollama。

image.png

1. 简介

1.1 为什么使用 Docker 部署?

优势 说明
环境隔离 不污染宿主机环境,依赖问题少
一键部署 容器化部署,跨平台一致性好
易于管理 重启、更新、迁移方便
资源控制 可限制内存、CPU 使用
适合生产 稳定可靠,推荐生产环境使用

1.2 硬件要求

模型规模 内存要求 推荐配置
0.5B-3B 2-4GB 最低 2GB 可用内存
7B-14B 8-16GB 最低 8GB 可用内存
30B+ 32GB+ 最低 32GB 可用内存

1.3 低配服务器(<2GB 内存)

如果你的服务器内存不足(如 1GB-2GB),运行大模型会遇到以下错误:

Error: 500 Internal Server Error: llama runner process has terminated: signal: killed

什么是 Swap?

Swap 是 Linux 系统中的一块硬盘空间,当作"备用内存"使用。当物理内存(RAM)不够用时,系统会把暂时不用的数据从内存搬到 Swap 中,腾出物理内存给需要运行的程序。

┌─────────────────────────────────────────────────┐
│  物理内存 (RAM)     =  你的办公桌(快速但小)    │
│  Swap (虚拟内存)    =  旁边的储物柜(慢但大)    │
│                                                 │
│  当办公桌放满东西时:                            │
│  把不常用的文件 → 放到储物柜 (Swap)             │
│  腾出空间 → 放置正在处理的文件                  │
└─────────────────────────────────────────────────┘

Swap 的作用

作用 说明
防止系统崩溃 内存不足时,用 Swap 补充,避免进程被杀死
运行大程序 允许运行超出物理内存的程序(如大语言模型)
内存回收 把不活跃的内存页面移到 Swap,释放物理内存

为什么需要 Swap?

你的服务器配置:
- 物理内存:1.8GB
- 想运行:3b 模型(需要 ~4GB 内存)

没有 Swap:
1.8GB < 4GB → 程序被杀死 ❌

有 5GB Swap:
1.8GB + 5GB = 6.8GB > 4GB → 可以运行 ✅

注意:使用 Swap 会牺牲性能(硬盘速度约为内存的 1/100),但总比程序崩溃好。

添加 Swap 虚拟内存

# 创建 4GB swap 文件
dd if=/dev/zero of=/swapfile bs=1M count=4096
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile

# 永久生效
echo '/swapfile none swap sw 0 0' >> /etc/fstab

# 验证
free -h

不同内存配置的模型推荐

服务器内存 推荐模型 Swap 需求
1GB qwen2.5-coder:0.5b 建议 2GB
2GB qwen2.5-coder:0.5b / 1.5b 建议 3GB
4GB qwen2.5-coder:3b 不需要
8GB+ qwen2.5-coder:7b 不需要

Swap 性能判断

Swap 使用量 状态 建议
0-500MB 正常 无需处理
500MB-1GB 一般 注意性能
1GB-2GB 较慢 考虑换小模型
>2GB 很慢 必须换小模型

内存监控命令

# 查看当前内存和 Swap 状态
free -h

# 实时监控内存(每 1 秒刷新)
watch -n 1 free -h

# 查看 Docker 容器资源使用
docker stats ollama

# 查看容器内存限制
docker inspect ollama | grep -i memory

# 查看系统内存配置
cat /proc/sys/vm/overcommit_memory
# 0 = 启发式过度分配(默认)
# 1 = 始终允许过度分配
# 2 = 严格控制,不允许过度分配

运行模型时实时监控

开启两个终端窗口:

终端 1:运行模型

docker exec -it ollama ollama run qwen2.5-coder:0.5b

终端 2:实时监控

watch -n 1 'free -h && echo "---" && docker stats ollama --no-stream'

常见问题排查

问题:模型运行时被杀死

# 1. 检查容器内存限制
docker inspect ollama | grep -i memory

# 2. 如果有内存限制,重新创建容器
docker rm -f ollama
docker run -d \
  -p 11434:11434 \
  --name ollama \
  --restart always \
  --memory-swap=-1 \
  ollama/ollama:latest

# 3. 启用内存过度分配
echo 1 | sudo tee /proc/sys/vm/overcommit_memory
echo 'vm.overcommit_memory = 1' | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

# 4. 重启容器
docker restart ollama

问题:Swap 使用过高导致卡顿

# 查看当前 Swap 使用
free -h

# 如果 Swap 使用 > 1GB,建议切换到更小的模型
docker exec -it ollama ollama run qwen2.5-coder:0.5b

2. 安装 Docker

2.1 Ubuntu/Debian

# 一键安装 Docker
curl -fsSL https://get.docker.com | sh

# 将当前用户加入 docker 组(免 sudo)
sudo usermod -aG docker $USER

# 重新登录或执行以下命令使组权限生效
newgrp docker

# 验证安装
docker --version

2.2 CentOS/RHEL

# 安装 Docker
sudo yum install -y docker

# 启动 Docker 服务
sudo systemctl start docker
sudo systemctl enable docker

# 将当前用户加入 docker 组
sudo usermod -aG docker $user

# 验证安装
docker --version

2.3 验证 Docker 安装

# 运行测试容器
docker run hello-world

# 查看 Docker 版本
docker --version
docker info

3. 部署 Ollama 容器

3.1 拉取镜像

# 拉取最新版 Ollama 镜像
docker pull ollama/ollama:latest

# 或指定版本
docker pull ollama/ollama:0.5.7

3.2 启动容器

CPU 模式(默认):

docker run -d \
  -p 11434:11434 \
  --name ollama \
  --restart always \
  ollama/ollama:latest

GPU 模式(需要 NVIDIA GPU):

# 首先安装 NVIDIA Container Toolkit
distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | sudo apt-key add -
curl -s -L https://nvidia.github.io/nvidia-docker/$distribution/nvidia-docker.list | \
  sudo tee /etc/apt/sources.list.d/nvidia-docker.list

sudo apt-get update
sudo apt-get install -y nvidia-container-toolkit
sudo systemctl restart docker

# 启动带 GPU 的容器
docker run -d \
  --gpus all \
  -p 11434:11434 \
  --name ollama \
  --restart always \
  ollama/ollama:latest

3.3 验证容器运行

# 查看容器状态
docker ps

# 查看容器日志
docker logs -f ollama

# 测试 API
curl http://localhost:11434/api/tags

4. 模型管理

4.1 拉取模型

# 拉取 qwen2.5-coder:3b
docker exec -it ollama ollama pull qwen2.5-coder:3b

# 拉取其他模型
docker exec -it ollama ollama pull qwen2.5:7b
docker exec -it ollama ollama pull deepseek-r1:7b

4.2 查看已安装模型

docker exec -it ollama ollama list

4.3 运行模型(交互式)

docker exec -it ollama ollama run qwen2.5-coder:3b

4.4 删除模型

docker exec -it ollama ollama rm qwen2.5-coder:3b

4.5 推荐模型

模型 用途 内存需求
qwen2.5-coder:0.5b 代码生成(轻量) ~1GB
qwen2.5-coder:3b 代码生成(推荐) ~4GB
qwen2.5-coder:7b 代码生成(专业) ~8GB
qwen2.5:3b 通用对话 ~4GB
qwen2.5:7b 通用对话(推荐) ~8GB

5. API 调用

5.1 基础调用格式

# 生成文本
curl http://localhost:11434/api/generate -d '{
  "model": "qwen2.5-coder:3b",
  "prompt": "用python写一个快速排序",
  "stream": false
}'

# 对话模式
curl http://localhost:11434/api/chat -d '{
  "model": "qwen2.5-coder:3b",
  "messages": [
    {"role": "user", "content": "你好"}
  ],
  "stream": false
}'

5.2 参数说明

参数 类型 说明 默认值
model string 模型名称 -
prompt string 输入文本 -
stream boolean 是否流式输出 true
temperature number 温度(0-1),越高越随机 0.8
num_ctx number 上下文长度 2048

5.3 Python 调用示例

import requests

API_URL = "http://localhost:11434/api/generate"

def call_ollama(prompt: str, model: str = "qwen2.5-coder:3b"):
    response = requests.post(API_URL, json={
        "model": model,
        "prompt": prompt,
        "stream": False
    })
    return response.json()["response"]

# 使用
result = call_ollama("用python写一个快速排序")
print(result)

5.4 JavaScript 调用示例

浏览器环境(原生 Fetch)

// 非流式响应
async function callOllama(prompt) {
  const response = await fetch("http://localhost:11434/api/generate", {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      model: "qwen2.5-coder:3b",
      prompt: prompt,
      stream: false
    })
  });

  const data = await response.json();
  return data.response;
}

// 使用
callOllama("用python写一个快速排序").then(console.log);

流式响应(浏览器)

async function chatWithOllama(prompt) {
  const response = await fetch("http://localhost:11434/v1/chat/completions", {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      model: "qwen2.5-coder:3b",
      messages: [{ role: "user", content: prompt }],
      stream: true
    })
  });

  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let result = "";

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    const chunk = decoder.decode(value);
    const lines = chunk.split("\n").filter(line => line.trim());

    for (const line of lines) {
      if (line.startsWith("data: ")) {
        const data = line.slice(6);
        if (data === "[DONE]") continue;
        try {
          const json = JSON.parse(data);
          const content = json.choices?.[0]?.delta?.content;
          if (content) {
            result += content;
            console.log(content);  // 实时输出
          }
        } catch (e) {
          // 忽略解析错误
        }
      }
    }
  }
  return result;
}

// 使用
chatWithOllama("用python写一个快速排序");

Node.js 环境

const axios = require("axios");

async function callOllama(prompt) {
  const response = await axios.post(
    "http://localhost:11434/api/generate",
    {
      model: "qwen2.5-coder:3b",
      prompt: prompt,
      stream: false
    }
  );

  return response.data.response;
}

// 使用
callOllama("用python写一个快速排序").then(console.log);

带认证的调用

// 如果设置了 API 密钥
async function callOllamaWithAuth(prompt) {
  const response = await fetch("http://localhost:11434/api/generate", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Authorization": "Bearer your_api_key_here"
    },
    body: JSON.stringify({
      model: "qwen2.5-coder:3b",
      prompt: prompt,
      stream: false
    })
  });

  const data = await response.json();
  return data.response;
}

5.5 OpenAI 兼容格式(JavaScript)

// 使用 OpenAI SDK 调用 Ollama
import OpenAI from 'openai';

const client = new OpenAI({
  baseURL: "http://localhost:11434/v1",
  apiKey: "ollama"  // 不需要真实 key
});

async function chat(prompt) {
  const response = await client.chat.completions.create({
    model: "qwen2.5-coder:3b",
    messages: [{ role: "user", content: prompt }]
  });

  return response.choices[0].message.content;
}

// 使用
chat("用python写一个快速排序").then(console.log);

5.6 外网调用示例

// 如果配置了外网访问(需要 HTTPS + API Key)
async function callOllamaRemote(prompt) {
  const response = await fetch("https://your-domain.com/api/generate", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Authorization": "Bearer your_secure_password"
    },
    body: JSON.stringify({
      model: "qwen2.5-coder:3b",
      prompt: prompt,
      stream: false
    })
  });

  const data = await response.json();
  return data.response;
}

6. 容器管理

6.1 查看容器状态

# 查看运行中的容器
docker ps

# 查看所有容器(包括停止的)
docker ps -a

# 查看容器详细信息
docker inspect ollama

6.2 日志管理

# 查看实时日志
docker logs -f ollama

# 查看最近 100 行日志
docker logs --tail 100 ollama

# 查看带时间戳的日志
docker logs -t ollama

6.3 启停重启

# 停止容器
docker stop ollama

# 启动容器
docker start ollama

# 重启容器
docker restart ollama

# 删除容器(需先停止)
docker rm -f ollama

6.4 进入容器

# 进入容器 shell
docker exec -it ollama bash

# 在容器中执行命令
docker exec -it ollama ollama list

7. 进阶配置

7.1 持久化模型存储

默认情况下,模型存储在容器内部,删除容器后模型会丢失。使用挂载卷持久化:

# 删除旧容器
docker rm -f ollama

# 重新创建,挂载本地目录
docker run -d \
  -p 11434:11434 \
  -v ollama_data:/root/.ollama \
  --name ollama \
  --restart always \
  ollama/ollama:latest

7.2 资源限制

# 限制内存使用为 4GB
docker run -d \
  -p 11434:11434 \
  --memory=4g \
  --name ollama \
  --restart always \
  ollama/ollama:latest

# 限制 CPU 使用
docker run -d \
  -p 11434:11434 \
  --cpus=2.0 \
  --name ollama \
  --restart always \
  ollama/ollama:latest

7.3 环境变量配置

docker run -d \
  -p 11434:11434 \
  -e OLLAMA_HOST=0.0.0.0:11434 \
  -e OLLAMA_NUM_PARALLEL=4 \
  -e OLLAMA_DEBUG=0 \
  -v ollama_data:/root/.ollama \
  --name ollama \
  --restart always \
  ollama/ollama:latest

7.4 使用 Docker Compose

创建 docker-compose.yml

version: '3.8'

services:
  ollama:
    image: ollama/ollama:latest
    container_name: ollama
    ports:
      - "11434:11434"
    volumes:
      - ollama_data:/root/.ollama
    environment:
      - OLLAMA_HOST=0.0.0.0:11434
      - OLLAMA_NUM_PARALLEL=4
    restart: always
    # GPU 配置(需要 nvidia-docker)
    # deploy:
    #   resources:
    #     reservations:
    #       devices:
    #         - driver: nvidia
    #           count: all
    #           capabilities: [gpu]

volumes:
  ollama_data:

启动:

docker-compose up -d

7.5 国内镜像加速

# 使用国内镜像源
docker pull registry.cn-hangzhou.aliyuncs.com/ollama/ollama:latest

# 或使用代理
docker pull ollama/ollama:latest

8. 故障排查

8.1 容器启动失败

# 查看容器日志
docker logs ollama

# 常见错误:GPU 配置问题
# 解决方案:删除容器,使用 CPU 模式重新创建
docker rm -f ollama
docker run -d -p 11434:11434 --name ollama --restart always ollama/ollama:latest

8.2 无法访问 API

# 检查容器是否运行
docker ps

# 检查端口是否正确映射
docker port ollama

# 测试容器内部 API
docker exec ollama curl http://localhost:11434/api/tags

# 检查防火墙
sudo ufw status  # Ubuntu
sudo firewall-cmd --list-all  # CentOS

8.3 模型加载慢

# 查看资源使用情况
docker stats ollama

# 检查磁盘 IO
docker exec ollama df -h

8.4 内存不足

# 查看容器资源使用
docker stats --no-stream

# 使用更小的模型
docker exec -it ollama ollama pull qwen2.5-coder:0.5b

# 或限制容器内存
docker update --memory=4g ollama

9. 生产部署建议

9.1 安全配置

# 绑定到本地地址
docker run -d \
  -p 127.0.0.1:11434:11434 \
  --name ollama \
  ollama/ollama:latest

# 使用反向代理(Nginx)配置 HTTPS

9.2 监控配置

# 使用 Prometheus + Grafana 监控
docker run -d \
  --name prometheus \
  -p 9090:9090 \
  prom/prometheus

# 配置 cAdvisor 监控容器
docker run -d \
  --name cadvisor \
  -p 8080:8080 \
  google/cadvisor:latest

9.3 高可用配置

# 使用负载均衡
# 部署多个 Ollama 实例,通过 Nginx 负载均衡

# 使用健康检查
docker run -d \
  --name ollama \
  --health-cmd="curl -f http://localhost:11434/api/tags || exit 1" \
  --health-interval=30s \
  --health-timeout=10s \
  --health-retries=3 \
  ollama/ollama:latest

10. 常用命令速查

# 拉取模型
docker exec -it ollama ollama pull qwen2.5-coder:3b

# 查看模型列表
docker exec -it ollama ollama list

# 运行模型
docker exec -it ollama ollama run qwen2.5-coder:3b

# 查看日志
docker logs -f ollama

# 重启容器
docker restart ollama

# 进入容器
docker exec -it ollama bash

# 删除容器
docker rm -f ollama

# 测试 API
curl http://localhost:11434/api/tags

你不知道的 TypeScript:联合类型与分布式条件类型

在 TypeScript 中,联合类型是非常常用的类型工具,但联合类型在条件类型中的分布式特性,估计会困扰很多人,因为它的行为非常的…不直观。

所以本文尽量用简单的语言和丰富的例子来让大家彻底搞懂联合类型与分布式条件类型,搞不懂也别打我。

联合类型

联合类型(Union Types) 表示一个值可以是几种类型之一。用竖线 | 来分隔每个类型,例如 string | number 表示一个值可以是 stringnumber

联合类型的基础用法:

// 表示类型可以为 string 或 number
type StringOrNumber = string | number;

let value: StringOrNumber;
value = "hello";  // ✅
value = 42;       // ✅
value = true;     // ❌ 不能将类型“boolean”分配给类型“StringOrNumber”

// 多个类型的联合
type ID = string | number | symbol;

// 字面量类型的联合
type Status = "pending" | "success" | "error";
type StyleType = 1 | 2 | 3;

分布式条件类型

什么是条件类型?

条件类型(Conditional Types)的语法类似于 JavaScript 中的三元运算符:

SomeType extends OtherType ? TrueType : FalseType;

它表示:当 extends 左边的类型 SomeType 可以赋值给右侧的类型 OtherType 时,返回 TrueType,否则返回 FalseType

用法示例:

type A = string extends 'string' ? true : false // false
type B = 'string' extends string ? true : false // true
type C = number extends number ? boolean : string // boolean

实际应用中,条件类型常配合泛型一起使用:

// 判断一个类型是否为字符串类型
type IsString<T> = T extends string ? true : false;
type A = IsString<string>;   // true
type B = IsString<number>;   // false

// 提取函数的返回类型
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type R = MyReturnType<() => string>;  // string

什么是分布式条件类型?

When conditional types act on a generic type, they become distributive when given a union type.

分布式条件类型(Distributive Conditional Types) 是指:当条件类型作用于泛型类型时,如果该泛型类型为联合类型,则条件类型将具有 分布性

简单来说,就是条件类型会分别对联合类型的每个成员进行判断和处理,然后将所有结果重新组合成一个联合类型。我愿意把这理解为 TypeScript 类型系统中的 "forEach"

假设我们有下面的类型 ToArray,可以看到它会把联合类型泛型当做一个整体处理。

type ToArray<T> = T[]
type Result = ToArray<string | number>; // (string | number)[]

而如果我们使用条件类型,可以看到此时联合类型的每一个成员会分别应用于条件类型。

type ToArray<T> = T extends any ? T[] : never;

// 当 T 是联合类型时
type Result = ToArray<string | number>; // string[] | number[]

// 分布过程:
// ToArray<string | number>
// =>  ToArray<string> | ToArray<number>
// => (string extends any ? string[] : never) | (number extends any ? number[] : never)
// => string[] | number[]

这里 T extends any 几乎永远为真(除了 Tnever 的特殊情况),其主要作用是触发分布式条件类型。我们也可以写成 T extends unknownT extends T 等形式来保证条件为真。

但是必须要注意一点:extends 的左边必须为单独的 T

阻止分布式行为

在某些场景下,我们希望在条件类型中使用联合类型,但不希望触发分布式行为,此时可以用方括号 []extends 关键字两侧的类型包裹起来。

type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never;

// 此时 ArrOfStrOrNum 不再是一个联合类型
type ArrOfStrOrNum = ToArrayNonDist<string | number>; // (string | number)[]

我们将 extends 左边单独的类型参数(比如前面的 T)称为 Naked Type (裸类型,一般指没有被包装的类型)。只有 Naked Type 才会触发分布式条件类型,当它不再是 Naked Type 就不会触发分布式。

事实上,任何形式的包装都可以阻止分布式,例如:

  • [T] extends [any] - 用元组包装
  • Array<T> extends Array<any> - 用泛型包装
  • { value: T } extends { value: any } - 用对象类型包装

只要 extends 左边不是单独的联合类型,就不会触发分布式行为。

分布式条件类型简单实例

Exclude

实现 TypeScript 内置的 Exclude<T, U> 类型:从联合类型 T 中排除 U 中的类型,来构造一个新的类型。

利用分布式条件类型的特性,依次判断联合类型 T 的每个成员是否可以赋值给 U。如果可以赋值,则返回 never(将其从结果中剔除);否则返回该成员本身(将其保留)。

type MyExclude<T, U> = T extends U ? never : T;

type T1 = MyExclude<"a" | "b" | "c", "a">; // "b" | "c"
type T2 = MyExclude<string | number | boolean, string>; // number | boolean

Extract

实现 TypeScript 内置的 Extract<T, U> 类型:从联合类型 T 中提取 U 中的类型,来构造一个新的类型。

Exclude 的逻辑完全相反:如果可以赋值给 U,则保留该成员;否则返回 never 将其剔除。

type MyExtract<T, U> = T extends U ? T : never;

type T3 = MyExtract<"a" | "b" | "c", "a" | "b">; // "a" | "b"
type T4 = MyExtract<string | number | boolean, number | boolean>; // number | boolean

Flatten

把联合类型中的每个数组元素都展平。

type Flatten<T> = T extends (infer U)[] ? U : T;

type Nested = string[] | number[];
type Flattened = Flatten<Nested>; // string | number

实战:实现 Permutation(全排列)

ok,学完 1+1,接下来我们开始学微积分了,笑:)

Type Challenges - Permutation

实现联合类型的全排列,将联合类型转换成所有可能的全排列数组的联合类型。

type perm = Permutation<'A' | 'B' | 'C'>; // ['A', 'B', 'C'] | ['A', 'C', 'B'] | ['B', 'A', 'C'] | ['B', 'C', 'A'] | ['C', 'A', 'B'] | ['C', 'B', 'A']

这是一道典型的分布式条件类型应用题。(谁会知道我为了这道醋包了这篇饺子)。

全排列问题的核心思路是递归(建议先理解 JavaScript 中的全排列实现)。

'A' | 'B' | 'C' 为例,递归的思路是:

  1. 枚举第一个位置的元素(ABC
  2. 对剩余元素递归求全排列
  3. 将当前元素与剩余元素的全排列组合

具体来说:

  • 当第一个元素为 A 时,剩余元素为 B | C,递归计算 Permutation<'B' | 'C'>
  • 当第一个元素为 B 时,剩余元素为 A | C,递归计算 Permutation<'A' | 'C'>
  • 当第一个元素为 C 时,剩余元素为 A | B,递归计算 Permutation<'A' | 'B'>

这里的重点是:利用分布式条件类型会自动遍历联合类型的每个成员的特点,枚举首元素

难点在于:如何获取"除当前元素外的剩余元素"?我们需要一个额外的泛型参数 U 来保存完整的联合类型。当分布式条件类型遍历时,T 是当前元素,U 是完整的联合类型,此时使用 Exclude<U, T> 就能得到剩余元素。

初步实现如下:

type Permutation<T, U = T> = 
  T extends any
  ? [T, ...Permutation<Exclude<U, T>>]
  : []

type R = Permutation<'A' | 'B' | 'C'>;

不幸的是,上面的实现会得到 never。原因是递归缺少终止条件

当递归到最后,T 会变成空的联合类型(即 never),但我们没有特殊处理终止条件,只能得到 never

我们需要添加一个终止条件判断:当 T 为空集时,不需要再处理直接返回 []

判断空集的条件是 [T] extends [never]。这里将 T 包裹在元组中,阻止了分布式行为,使其变成普通的类型检查。只有当 T 为空集时,这个条件才为真。

正确答案:

/**
 * 生成联合类型的所有排列组合
 */
type Permutation<T, U = T> = 
  [T] extends [never]
    ? []
    : T extends U
      ? [T, ...Permutation<Exclude<U, T>>]
      : never;

type P1 = Permutation<"a" | "b" | "c">;
// => ["a", "b", "c"] | ["a", "c", "b"] | ["b", "a", "c"] | ["b", "c", "a"] | ["c", "a", "b"] | ["c", "b", "a"]

参考资源

JSyncQueue——一个开箱即用的鸿蒙异步任务同步队列

一、简介

在鸿蒙应用开发中,异步任务的顺序执行是一个常见需求。当多个异步任务需要按照特定顺序执行时,如果不加控制,可能会导致执行顺序混乱。

项目地址:github.com/zincPower/J…

JSyncQueue 提供了一个简洁的解决方案:

  • 顺序执行保证:所有任务严格按照入队顺序执行,即使任务内部有异步操作也能保证顺序
  • 双模式支持:支持 "立即执行" 和 "延时执行",满足不同场景需求
  • 双任务模式:支持 "Message 消息模式" 和 "Runnable 闭包模式"
  • 任务取消和管理:可随时取消指定任务或清空整个队列
  • 任务结果:通过 getResult() 获取任务执行结果,支持 then/catch/finally
  • 可继承扩展:通过继承 JSyncQueue 并重写 onHandleMessage 方法,实现自定义消息处理逻辑

项目架构如下图所示:

二、安装

ohpm install jsyncqueue

三、快速开始

3-1、基础使用

可以直接使用 JSyncQueue 无需继承,但仅支持 Runnable 模式(post/postDelay)。

import { JSyncQueue } from 'jsyncqueue'

// 创建队列
const queue = new JSyncQueue("MyQueue")

// 添加任务
queue.post(async (taskId) => {
  // 执行异步操作
  const result = await someAsyncOperation()
  return result
}).getResult().then((result) => {
  console.log(`任务完成: ${result}`)
}).catch((error) => {
  console.error(`任务失败: ${error}`)
})

3-2、继承使用

继承 JSyncQueue 后,既可以使用 Message 模式(sendMessage/sendMessageDelay)处理消息,也可以使用 Runnable 模式(post/postDelay)执行闭包。

import { JSyncQueue, Message, Any } from 'jsyncqueue'

class MyQueue extends JSyncQueue {
  async onHandleMessage(message: Message, taskId: number): Promise<Any> {
    switch (message.what) {
      case "say_hello":
        const name = message.data["name"]
        return `你好,${name}!`
      default:
        return undefined
    }
  }
}

// 使用自定义队列
const queue = new MyQueue("MyQueue")
queue.sendMessage({
  what: "say_hello",
  data: { name: "小明" }
}).getResult().then((result) => {
  console.log(result) // 输出: 你好,小明!
})

四、核心概念

4-1、“立即执行” 和 “延时执行”

方法 说明
post(runnable) 立即将闭包加入队列执行
postDelay(runnable, delay) 延时指定毫秒后将闭包加入队列执行
sendMessage(message) 立即将消息加入队列执行
sendMessageDelay(message, delay) 延时指定毫秒后将消息加入队列执行

4-2、“Message 模式” 和 “Runnable 模式”

Runnable 模式:直接传入一个闭包函数,适合简单的一次性任务。

queue.post(async (taskId) => {
  // 直接在闭包中编写执行逻辑
  return "任务结果"
})

Message 模式:发送消息到队列,由 onHandleMessage 方法处理,适合需要集中管理业务逻辑的场景。

queue.sendMessage({
  what: "action_type",
  data: { key: "value" }
})

注意:直接使用 JSyncQueue 实例时,Message 模式的消息不会被处理(onHandleMessage 默认返回 undefined)。需要继承 JSyncQueue 并重写 onHandleMessage 方法才能处理消息。

五、API 文档

5-1、JSyncQueue 类

构造函数

constructor(queueName: string)

创建一个同步队列实例。

  • queueName: 队列名称,用于标识和调试

方法

方法 参数 返回值 说明
post(runnable) runnable: (taskId: number) => Promise<Any> Task 立即执行闭包
postDelay(runnable, delay) runnable: (taskId: number) => Promise<Any>, delay: number Task 延时执行闭包,delay 单位为毫秒
sendMessage(message) message: Message Task 立即发送消息
sendMessageDelay(message, delay) message: Message, delay: number Task 延时发送消息,delay 单位为毫秒
cancel(taskId) taskId: number void 取消指定任务
clear() - void 清空队列中所有等待的任务
dumpInfo() - string 获取队列调试信息
onHandleMessage(message, taskId) message: Message, taskId: number Promise<Any> 消息处理方法,子类可重写

属性

属性 类型 说明
queueName string 队列名称(只读)
length number 当前队列中的任务数量(只读)

5-2、Message 接口

interface Message {
  what: string   // 消息类型
  data: Any      // 消息数据
}

5-3、Task 接口

interface Task {
  cancel(): void                  // 取消任务
  getResult(): Promise<Any>       // 获取任务结果
  getTaskId(): number            // 获取任务 ID
}

5-4、异常类型

JSyncQueueCancelException

任务被取消时抛出的异常。

interface JSyncQueueCancelException {
  message: string
}

JSyncQueueException

队列内部错误时抛出的异常。

interface JSyncQueueException {
  message: string
}

六、使用示例

6-1、直接使用 JSyncQueue + post()

适用于简单场景,直接使用闭包处理任务。

import { JSyncQueue } from 'jsyncqueue'

const queue = new JSyncQueue("SimpleQueue")

// 添加多个任务,它们会按顺序执行
for (let i = 0; i < 5; i++) {
  queue.post(async (taskId) => {
    console.log(`开始执行任务 ${i}`)
    // 模拟异步操作
    await new Promise(resolve => setTimeout(resolve, 100))
    console.log(`完成任务 ${i}`)
    return `结果 ${i}`
  }).getResult().then((result) => {
    console.log(`任务 ${i} 返回: ${result}`)
  })
}

6-2、继承 JSyncQueue 自定义队列

适用于需要集中管理业务逻辑的场景,继承后同样支持 Runnable 模式。

import { JSyncQueue, Message, Any } from 'jsyncqueue'

class UserQueue extends JSyncQueue {
  private userCount = 0

  async onHandleMessage(message: Message, taskId: number): Promise<Any> {
    switch (message.what) {
      case "register":
        this.userCount++
        const name = message.data["name"]
        // 模拟异步注册操作
        await this.simulateAsyncOperation()
        return `用户 ${name} 注册成功,当前用户数: ${this.userCount}`

      case "login":
        const username = message.data["username"]
        await this.simulateAsyncOperation()
        return `用户 ${username} 登录成功`

      default:
        return undefined
    }
  }

  private async simulateAsyncOperation() {
    return new Promise(resolve => setTimeout(resolve, 100))
  }
}

// 使用
const userQueue = new UserQueue("UserQueue")

userQueue.sendMessage({
  what: "register",
  data: { name: "张三" }
}).getResult().then(console.log)

userQueue.sendMessage({
  what: "login",
  data: { username: "张三" }
}).getResult().then(console.log)

// 继承后同样可以使用 post()
userQueue.post(async (taskId) => {
  console.log("执行自定义闭包任务")
  return "闭包任务完成"
}).getResult().then(console.log)

6-3、延时执行示例

import { JSyncQueue } from 'jsyncqueue'

const queue = new JSyncQueue("DelayQueue")

// 延时 1 秒后执行
queue.postDelay(async (taskId) => {
  console.log("延时任务执行了")
  return "延时任务结果"
}, 1000).getResult().then((result) => {
  console.log(`延时任务返回: ${result}`)
})

// 延时发送消息(需要继承实现 onHandleMessage)
queue.sendMessageDelay({
  what: "delayed_action",
  data: { info: "延时消息" }
}, 2000)

6-4、任务取消示例

import { JSyncQueue, Task, JSyncQueueCancelException } from 'jsyncqueue'

const queue = new JSyncQueue("CancelQueue")

// 添加任务并保存引用
const task: Task = queue.post(async (taskId) => {
  console.log("任务开始执行")
  await new Promise(resolve => setTimeout(resolve, 5000))
  return "任务完成"
})

// 监听任务结果
task.getResult().then((result) => {
  console.log(`任务成功: ${result}`)
}).catch((error: JSyncQueueCancelException) => {
  console.log(`任务被取消: ${error.message}`)
})

// 取消任务(两种方式)
task.cancel()                    // 方式1:通过 Task 对象取消
// queue.cancel(task.getTaskId()) // 方式2:通过队列和任务ID取消

// 清空所有任务
// queue.clear()

6-5、混合使用示例

Message 和 Runnable 可以混合使用,它们都会按入队顺序执行。

import { JSyncQueue, Message, Any } from 'jsyncqueue'

class MixedQueue extends JSyncQueue {
  async onHandleMessage(message: Message, taskId: number): Promise<Any> {
    console.log(`处理消息: ${message.what}`)
    return `消息 ${message.what} 处理完成`
  }
}

const queue = new MixedQueue("MixedQueue")

// 混合添加任务
queue.post(async () => {
  console.log("Runnable 1")
  return "R1"
})

queue.sendMessage({ what: "msg1", data: null })

queue.post(async () => {
  console.log("Runnable 2")
  return "R2"
})

queue.sendMessage({ what: "msg2", data: null })

// 执行顺序:Runnable 1 -> msg1 -> Runnable 2 -> msg2

七、作者简介

掘金:juejin.im/user/5c3033…

csdn:blog.csdn.net/weixin_3762…

公众号:微信搜索 "江澎涌"

Windows 系统中 fnm 安装与配置指南

Windows 系统中 fnm 安装与配置指南

本文档介绍如何在 Windows 系统中安装 fnm (Fast Node Manager) 并配置 CMD、PowerShell 和 PowerShell 7 终端以自动加载 Node.js 环境。

前提条件

  • Windows 10 或更高版本
  • 已安装 WinGet(Windows 包管理器)

安装 fnm

使用 WinGet 安装 fnm:

winget install Schniz.fnm

配置终端

1. 配置 PowerShell 7

PowerShell 7 使用独立的配置文件路径。需要创建以下配置文件:

# 创建 PowerShell 7 配置目录
New-Item -ItemType Directory -Path "$env:USERPROFILE\Documents\PowerShell" -Force

# 创建配置文件
New-Item -ItemType File -Path "$env:USERPROFILE\Documents\PowerShell\Microsoft.PowerShell_profile.ps1" -Force

在配置文件中添加以下内容:

fnm env --use-on-cd | Out-String | Invoke-Expression

可以通过以下命令一次性完成:

New-Item -ItemType Directory -Path 'C:\Users\leehoo\Documents\PowerShell' -Force
Set-Content -Path 'C:\Users\leehoo\Documents\PowerShell\Microsoft.PowerShell_profile.ps1' -Value 'fnm env --use-on-cd | Out-String | Invoke-Expression'

2. 配置 Windows PowerShell

Windows PowerShell 使用以下配置文件路径:

C:\Users\leehoo\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1

如果文件不存在,创建它并添加以下内容:

fnm env --use-on-cd | Out-String | Invoke-Expression

3. 配置 CMD

CMD 没有配置文件机制,需要通过注册表设置 AutoRun。

步骤 1:生成 fnm 初始化脚本
fnm env --use-on-cd --shell cmd | ForEach-Object { '@' + $_ } | Out-File -FilePath "$env:APPDATA\fnm\fnm-init.cmd" -Encoding ASCII
步骤 2:设置注册表 AutoRun
reg add 'HKCU\Software\Microsoft\Command Processor' /v AutoRun /t REG_SZ /d "call $env:APPDATA\fnm\fnm-init.cmd" /f

验证配置

PowerShell 7 / Windows PowerShell

重新打开 PowerShell 终端,运行:

node --version
npm --version

CMD

重新打开 CMD 终端,运行:

node -v
npm -v

常用 fnm 命令

# 列出已安装的 Node.js 版本
fnm list

# 安装 Node.js 版本
fnm install 20.10.0

# 切换 Node.js 版本
fnm use 20.10.0

# 设置默认版本
fnm default 20.10.0

# 卸载版本
fnm uninstall 20.10.0

注意事项

  1. 每次修改配置后,需要重新打开终端才能生效
  2. --use-on-cd 选项会在切换目录时自动检测并使用对应的 Node.js 版本(基于 .nvmrc 或 package.json)
  3. CMD 的 AutoRun 设置会影响所有 CMD 窗口
  4. 如果需要移除 CMD 的 AutoRun 设置,运行:
reg delete 'HKCU\Software\Microsoft\Command Processor' /v AutoRun /f

配置文件位置总结

终端 配置文件路径
PowerShell 7 C:\Users\leehoo\Documents\PowerShell\Microsoft.PowerShell_profile.ps1
Windows PowerShell C:\Users\leehoo\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1
CMD 注册表:HKEY_CURRENT_USER\Software\Microsoft\Command Processor\AutoRun
fnm 初始化脚本 C:\Users\leehoo\AppData\Roaming\fnm\fnm-init.cmd

故障排除

CMD 中显示命令输出

如果 CMD 启动时显示 SET 命令,确保 fnm-init.cmd 文件每行都以 @ 开头:

@SET PATH=...
@SET FNM_MULTISHELL_PATH=...

Node.js 命令不可用

  1. 确认 fnm 已正确安装:fnm --version
  2. 确认已安装 Node.js 版本:fnm list
  3. 检查配置文件路径是否正确
  4. 重新打开终端

PowerShell 配置不生效

  1. 检查配置文件是否存在
  2. 运行 $PROFILE 查看当前配置文件路径
  3. 手动加载配置:. $PROFILE

开源一个 markdown-it 插件:让你的博客支持 GitHub 用户悬浮卡片

开源一个 markdown-it 插件:让你的博客支持 GitHub 用户悬浮卡片

前言

在写技术博客或文档时,我们经常需要提到 GitHub 上的开发者,比如「这个方案参考了 @antfu 的实现」。但读者看到这个 @ 时,往往需要点击跳转才能了解这个人是谁。

能不能像 GitHub 那样,鼠标悬停就能看到用户信息呢?

于是我开发了这个插件:markdown-it-github-mention-card

效果预览

在 Markdown 中写入:

这个项目的灵感来自 {@antfu} 的开源作品。

渲染后,鼠标悬停在链接上,就会出现一个精美的用户信息卡片,包含:

  • 用户头像
  • 用户名和简介
  • 地理位置、公司信息
  • 粉丝数、关注数、公开仓库数

无需跳转页面,信息一目了然。

特性

  • 语法简洁{@username} 即可,支持自定义显示文本
  • 悬浮卡片:调用 GitHub API 实时获取用户信息
  • 暗色模式:自动适配 .dark 主题类
  • SSR/SSG 友好:完美支持 VitePress、Nuxt、Next.js 等框架
  • TypeScript:完整的类型定义
  • 轻量无依赖:核心代码仅依赖 markdown-it

安装

# npm
npm install markdown-it-github-mention-card

# pnpm
pnpm add markdown-it-github-mention-card

# yarn
yarn add markdown-it-github-mention-card

基础使用

import MarkdownIt from 'markdown-it'
import MarkdownItGitHubMentionCard, { initHoverCard } from 'markdown-it-github-mention-card'

const md = new MarkdownIt()
md.use(MarkdownItGitHubMentionCard, {
  // 可选:提高 GitHub API 速率限制
  githubToken: 'YOUR_GITHUB_TOKEN',
})

// 渲染 Markdown
const html = md.render('{@antfu} 是 Vue 核心团队成员')
document.querySelector('#app').innerHTML = html

// 初始化悬浮卡片(浏览器端调用)
initHoverCard()

语法说明

插件支持三种写法:

语法 说明 示例
{@username} 基础用法 {@antfu} → 显示 "antfu"
{@username|显示文本} 自定义文本 {@antfu|Anthony Fu} → 显示 "Anthony Fu"
{@username|显示文本|链接} 自定义链接 {@antfu|博客|https://antfu.me}

SSR/SSG 场景最佳实践

在服务端渲染场景下,构建时和浏览器端环境分离,推荐以下方式:

服务端(构建时):

import MarkdownIt from 'markdown-it'
import MarkdownItGitHubMentionCard from 'markdown-it-github-mention-card'

const md = new MarkdownIt()
md.use(MarkdownItGitHubMentionCard)
// 服务端不传 token,避免暴露

const html = md.render('{@antfu}')

客户端(浏览器):

import { initHoverCard } from 'markdown-it-github-mention-card'

// VitePress / Vite
initHoverCard(import.meta.env.VITE_GITHUB_TOKEN)

// Next.js
initHoverCard(process.env.NEXT_PUBLIC_GITHUB_TOKEN)

暗色模式

插件内置暗色模式支持,当页面或父元素包含 .dark 类时,卡片自动切换暗色主题:

<html class="dark">
  <!-- 卡片自动适配暗色 -->
</html>

与 VitePress、Tailwind CSS 等主流方案完美兼容。

在 VitePress 中使用

// .vitepress/config.ts
import MarkdownItGitHubMentionCard from 'markdown-it-github-mention-card'

export default {
  markdown: {
    config: (md) => {
      md.use(MarkdownItGitHubMentionCard)
    }
  }
}
// .vitepress/theme/index.ts
import { initHoverCard } from 'markdown-it-github-mention-card'
import { onMounted } from 'vue'

export default {
  enhanceApp() {
    if (typeof window !== 'undefined') {
      onMounted(() => {
        initHoverCard(import.meta.env.VITE_GITHUB_TOKEN)
      })
    }
  }
}

实现原理

简单介绍一下核心实现:

  1. Markdown 解析:通过 markdown-it 的 inline rule,匹配 {@...} 语法,生成带有 data-github-user 属性的 <a> 标签

  2. 悬浮卡片initHoverCard() 在浏览器端监听鼠标事件,当悬停在目标链接上时:

    • 调用 GitHub REST API 获取用户信息
    • 动态创建卡片 DOM 并定位显示
    • 内置请求缓存,避免重复请求
  3. 样式注入:首次调用时自动注入 CSS,支持亮/暗两套主题

为什么不用 GitHub 官方的 Hover Card?

GitHub 的 Hover Card 仅在 github.com 域名下生效,且需要登录状态。本插件:

  • 可在任意网站使用
  • 无需用户登录 GitHub
  • 可自定义样式和行为
  • 适配各种 SSR/SSG 框架

项目地址

写在最后

这是我开源的一个小工具,希望能帮助到有类似需求的同学。

如果觉得有用,欢迎:

  • 给项目点个 ⭐ Star
  • 提 Issue 反馈问题或建议
  • 提 PR 一起完善

有任何问题欢迎在评论区交流讨论!

🔥 在浏览器地址栏输入 URL 后,页面是怎么一步步显示出来的?


这是一个前端面试 100% 会被问到的问题
但也是一个90% 的人答不完整的问题

你可能会说:

  • “DNS 解析”
  • “请求 HTML”
  • “解析 DOM”
  • “渲染页面”

👉 但如果继续追问:

  • CSS 为什么会阻塞渲染?
  • JS 为什么会卡住页面?
  • 回流和重绘到底差在哪?
  • 浏览器内核到底在干嘛?

很多人就开始“凭感觉回答了”。

这篇文章,我会用尽量通俗、不堆术语的方式,带你完整走一遍:

从你敲下回车,到页面真正出现在屏幕上,中间到底发生了什么?


一、先给结论:浏览器做了哪几件大事?

不讲细节,先给你一条完整主线👇

输入 URL → 页面展示,大致分 9 步:

  1. 解析 URL(域名 / IP)
  2. DNS 解析(域名 → IP)
  3. 向服务器请求 HTML(通常是 index.html)
  4. 解析 HTML,生成 DOM Tree
  5. 解析 CSS,生成 CSSOM Tree
  6. DOM + CSSOM → Render Tree
  7. Layout(计算位置和大小)
  8. Paint(绘制像素)
  9. Composite(图层合成,GPU 加速)

你现在只需要记住一句话:

浏览器做的事情,本质上就是:
把“代码”一步步变成“像素”。

后面我们逐个拆。


二、URL、域名、IP、DNS:浏览器是怎么找到服务器的?

1️⃣ IP 是什么?

一句话:

IP 地址 = 服务器在互联网上的门牌号

比如:
101.34.243.124

  • 公网 IP 在整个互联网中是唯一的
  • 只要你知道 IP,就能直接访问服务器

2️⃣ 那为什么还要域名?

因为 IP:

  • 难记
  • 不符合人类直觉

所以就有了:

  • google.com
  • baidu.com
  • juejin.cn

👉 域名,本质上就是 IP 的“别名”


3️⃣ DNS 到底在干嘛?

DNS 只干一件事:

把「好记的域名」翻译成「真实的 IP 地址」

流程非常简单:

你输入 juejin.cn
↓
DNS 查询
↓
得到一个 IP
↓
浏览器去这个 IP 对应的服务器请求资源

4️⃣ 公网 IP 和私有 IP 的区别

  • 公网 IP

    • 全网唯一
    • 能被外部访问
  • 私有 IP

    • 只在局域网内有效
    • 学校 / 公司 / 家庭常见


三、为什么浏览器一上来就请求 index.html?

你有没有想过一个问题:

我明明只输入了域名,
为什么服务器知道要返回 index.html?

原因很简单:

  • 浏览器访问服务器后
  • 默认请求一个入口文件
  • 这个文件几乎永远叫:index.html

所以你会发现:

  • Vue / React 项目最终都会打包出 index.html
  • 服务器部署的,其实是一堆静态资源
  • HTML 是一切渲染的起点

四、浏览器内核到底是什么?为什么老爱被问?

很多人会说:

  • Chrome 是 Blink 内核
  • Firefox 是 Gecko
  • Safari 是 WebKit

内核到底是啥?

一句话解释:

浏览器内核 = 负责解析 HTML / CSS / JS,并把页面渲染出来的核心模块

也叫:渲染引擎(Rendering Engine)

常见关系👇

浏览器 内核
Chrome / Edge / Opera Blink
Safari WebKit
Firefox Gecko
IE Trident(已淘汰)


五、浏览器是如何一步步把页面“画”出来的?

这一部分是整个问题的核心

1️⃣ 解析 HTML → DOM Tree

  • HTML 会被拆成一个个标签
  • 标签会被转换成节点
  • 最终形成一棵 DOM 树

👉 DOM 树描述的是:页面的结构


2️⃣ 解析 CSS → CSSOM Tree

  • 遇到 <link>,浏览器会下载 CSS
  • CSS 会被解析成 CSSOM 树

⚠️ 重点来了:

CSS 不会阻塞 DOM 的解析
会阻塞页面的渲染


3️⃣ DOM + CSSOM → Render Tree

  • Render Tree 只包含需要显示的节点
  • display: none 的元素不会进入渲染树

👉 Render Tree 描述的是:页面真正要画什么


4️⃣ Layout:计算位置和大小

Layout 阶段,浏览器会计算:
每个元素在哪?多大?


5️⃣ Paint:真正开始画了

把布局结果,转换为屏幕上的像素


6️⃣ Composite:图层合成(性能关键)

  • 页面会被拆成多个图层
  • GPU 参与合成
  • transform / opacity / video 等会创建新图层

👉 合理使用能提升性能,滥用会吃内存


六、回流 & 重绘:为什么页面会卡?

🔁 回流(Reflow)

一句话:

元素的位置或尺寸发生变化

常见触发场景:

  • 改 width / height
  • 改 position / display
  • DOM 结构变化
  • 读取布局信息(如 getComputedStyle

⚠️ 回流一定会触发重绘


🎨 重绘(Repaint)

一句话:

只改外观,不改布局

例如:

  • color
  • background-color
  • box-shadow
  • opacity

👉 成本比回流小得多


🚀 常见性能优化建议

  • 一次性修改样式(class / cssText)
  • 减少 DOM 操作
  • 避免频繁读取布局信息
  • 合理使用 position: absolute / fixed
  • 谨慎创建合成层

七、最后用一句话总结

浏览器渲染的本质就是:
HTML → DOM → CSSOM → Render Tree → Layout → Paint → Composite

如果你真正理解了这条链路:

  • 白屏问题
  • 页面卡顿
  • 动画掉帧
  • script / link 阻塞
  • 回流 & 重绘优化

都会变得非常清晰


下篇预告

这篇我们讲的是:

浏览器 & 渲染引擎

但还有一个主角没登场:

👉 JavaScript 引擎(V8)

下篇会聊:

  • JS 是谁执行的?
  • JS 为什么会阻塞渲染?
  • 浏览器内核和 JS 引擎的关系?

Elpis 项目 Webpack 配置详解

Elpis 项目 Webpack 配置详解

本指南详细介绍了 Elpis 项目的 Webpack 5 构建系统设置,涵盖了从基础配置到生产环境优化的核心逻辑。

🏗️ 1. 核心架构

项目采用 分层配置 模式,通过 webpack-merge 组合配置:

webpack.base.js: 基础配置,包含入口、解析路径、公用 Loader 和插件。

webpack.prod.js: 生产环境配置,侧重于性能优化、代码分割和资源压缩。

webpack.dev.js: 开发环境配置,侧重于开发体验、热更新和调试支持。

📦 2. 基础配置说明 (webpack.base.js)

2.1 动态多入口 (Multi-Entry)

项目通过 glob 自动扫描 app/pages/ */entry. .js:

自动化: 每增加一个页面只需按照命名规范创建入口文件,Webpack 会自动识别。

模板关联: 利用 HtmlWebpackPlugin 为每个入口生成对应的 .tpl 模板文件。

2.2 核心 Loader 规则

Vue 支持: 使用 vue-loader 处理 .vue 文件。

JS 转译: babel-loader 处理 ES6+ 语法,范围锁定在 ./app/pages 以提升速度。

样式处理:

支持 css 和 less。

生产环境下样式会被提取到独立文件,开发环境下通过 style-loader 内联。

资产模块 (Asset Modules):

使用 Webpack 5 的 asset 类型(替代 url-loader)。

内联策略: 小于 8KB 的图片自动转为 Base64 以减少 HTTP 请求。

2.3 解析与快捷路径 (Resolve)

别名配置: @pages: app/pages

@common: app/pages/common

@widgets: app/pages/widgets

@store: app/pages/store

🚀 3. 生产环境优化 (webpack.prod.js)

3.1 构建加速 (Speed)

多进程打包 (HappyPack): 利用多核 CPU 并行处理 JS 和 CSS 转译。

代码压缩: TerserWebpackPlugin 开启并行压缩,并移除生产环境的 console.log。

3.2 资源优化 (Asset Optimization)

代码分割 (Code Splitting):

vendor: 独立打包第三方依赖(axios, lodash 等)。

common: 提取被多次引用的业务公共模块。

runtimeChunk: 提取 Webpack 运行时代码,确保长效缓存。

CSS 提取: 使用 MiniCssExtractPlugin 提取样式,配合 CSSMinimizerPlugin 进行压缩。

3.3 目录清理

配置了 CleanWebpackPlugin,保证每次 build 产物目录都是干净的。

🌗 4. 开发环境 vs 生产环境深度对比

特性 开发环境 (Development) 生产环境 (Production)

核心目标 极致的开发体验与调试效率 极致的性能加载与线上稳定性

Mode mode: "development" (不压缩,原始名) mode: "production" (Tree Shaking,代码混淆)

Source Map eval-cheap-module-source-map (代码映射,方便定位) 通常禁用或使用独立文件 (保护源码,减小体积)

HMR 热更新 开启。代码保存即生效,不刷新页面且保持状态 关闭。代码全量打包,生成版本化静态资源

样式处理 内联注入 ,构建速度快 提取独立 CSS 文件,并行下载并利用缓存

文件指纹 通常不带 Hash,或仅带简单的 contenthash 强缓存策略:[chunkhash:8] 确保版本管理稳定

调试信息 保留所有的 console.log 和注释 自动清理:Terser 插件移除所有调试输出

网络请求 指向本地 DevServer (http://localhost:9002) 指向 CDN 或生产静态资源路径

🛠️ 5. 开发环境配置 (webpack.dev.js) 深度解析

该文件是开发阶段的核心,其核心逻辑在于建立浏览器与本地服务器的通信实时链路。

5.1 核心代码逻辑拆解
  1. 动态注入 HMR 客户端
Object.keys(baseConfig.entry).forEach((v) => {
  if (v !== "vendor") {
    const { HOST, PORT, HMR_PATH, TIMEOUT } = DEV_SERVER_CONFIG;
    baseConfig.entry[v] = [
      baseConfig.entry[v],
      // 关键:向每个业务入口注入 HMR 运行时的客户端代码
      `webpack-hot-middleware/client?path=http://${HOST}:${PORT}/${HMR_PATH}?timeout=${TIMEOUT}&reload=true`,
    ];
  }
});

为什么要做这一步? Webpack 默认打包出来的 JS 是静态的。为了实现热更新,我们需要在浏览器里运行一段“监听代码”,这段代码通过 EventSource 连接到开发服务器。注入后,浏览器就知道如何接收服务器发来的更新信号。

  1. 调试利器:Source Map

devtool: "eval-cheap-module-source-map", 原理:eval 将每个模块包裹在 eval 字符串中,构建极快;cheap 忽略列信息只保留行信息;module 负责把 Loader(如 vue-loader)处理前的源代码映射出来。 效果:你在浏览器 F12 看到的是原汁原味的 .vue 文件,而不是编译后的 JS。

  1. HMR 核心插件
plugins: [
  new HotModuleReplacementPlugin(), // 开启 HMR API
]

这个插件会在全局注入 module.hot 对象。只有有了这个对象,vue-loader 等程序才能调用 module.hot.accept() 接口来实现局部替换。

🔥 6. 热更新 (HMR) 核心原理深度阐述

HMR 的核心不是“刷新”,而是“补丁式替换”。

6.1 底层通信流程

Server 端 (监视者): Webpack 以 watch 模式启动。当你保存文件,Webpack 重新编译。

它不会生成新的大文件,而是生成两个小补丁:一个 [hash].hot-update.json(描述哪些模块变了)和一个 [chunk].[hash].hot-update.js(具体的变化代码)。

Middleware (快递员): webpack-hot-middleware 通过一条长连接(EventSource/WebSocket)推送一个消息给浏览器:“新版本 Hash 是 XXX”。

Client 端 (接收者): 浏览器里的 HMR Runtime 收到 Hash。

比对:发现和当前 Hash 不同。

下载清单:先下 .json 文件确认有哪些模块更新了。

载入补丁:动态创建

Runtime (施工员):

查找接口:Webpack 运行环境会查找代码中是否定义了 module.hot.accept。

代码替换:如果定义了(Vue 和 React 的 Loader 都会自动帮你加上),它会把内存中旧的模块定义删掉,换成补丁里的新定义,并重新执行该模块。

🍕 7. 代码分割

代码分割是 Webpack 优化中最关键的一环。核心目标是:不要让用户一次性下载一个超大的 JS 文件,而是将其拆分成多个小文件,按需加载或利用并发下载。

7.1 为什么要进行代码分割?

利用并发下载:浏览器可以同时下载多个小文件,比下载一个大文件快。

缓存:将几乎不动的第三方库(Vue, Lodash)和经常变动的业务代码分开。当你改了业务代码,用户只需要重新下载几 KB 的业务包,而几十 MB 的第三方库依

然使用浏览器缓存。

按需加载:只加载当前页面需要的代码,减少首屏负担。

7.2 项目中的 SplitChunks 配置拆解

在 webpack.base.js 的 optimization 中,配置了三个关键部分:

  1. vendor (第三方库)

配置: test: /[/]node_modules[/]/ 作用: 专门把从 node_modules 引入的所有库打包进 vendor.js。 策略: 这些库(如 Element Plus, Axios)版本通常是固定的,适合设置强缓存(Max-Age 一年)。

  1. common (公用业务代码)

配置: minChunks: 2, minSize: 1 作用: 如果你写了一个 utils.js 工具函数,且被两个以上的页面入口 import 了,它就会被自动提取到 common.js 中。 优点: 防止重复打包。如果没有这一步,每个页面的 bundle 里都会包含一份一模一样的工具函数。

  1. runtimeChunk: true (运行时代码)

作用: 这会生成一个类似 runtime~main.js 的微型文件。 深度原理解析: Webpack 打包时会给每个模块分配 ID。模块 A 引用模块 B 时,内部记录的是 ID。 问题: 如果不提取 runtime,模块 B 变了,模块 A 里的 ID 映射也会变,导致模块 A 的 Hash 也失效。 解决: 提取 runtime 后,所有的模块映射关系都存在这个小文件里。哪怕业务代码变了,只要模块依赖关系没变,其他文件的 Hash 就能保持稳定。

7.3 代码分割示意图 (前后对比)

打包方式 产物结构 浏览器行为

不分割 main.js (2MB) 改一行代码,用户得重下 2MB。

分割后 vendor.js (1.8MB) + common.js (100KB) + page.js (10KB) 改一行代码,用户只重下 10KB。

⚡ 8. 建议优化方向

更换现代化 Loader: 将 HappyPack 迁移至 thread-loader。

TypeScript:为 JavaScript 注入类型安全的工程化力量

JavaScript 以其灵活、动态的特性成为 Web 开发的基石,但这种“自由”在大型项目中往往演变为隐患。当函数参数类型不明、对象结构随意扩展、变量用途模糊不清时,代码便如同没有护栏的悬崖——看似畅通无阻,实则危机四伏。TypeScript(TS)作为 JavaScript 的超集,通过引入静态类型系统,在保留 JS 灵活性的同时,为开发者构建起一道坚固的质量防线。

弱类型的代价:隐藏在“简单”背后的陷阱

JavaScript 是动态弱类型语言,变量类型在运行时才确定。这使得以下代码合法却危险:

function add(a, b) {
  return a + b;
}
const result = add(10, '10'); // "1010"(字符串拼接)

开发者本意是数值相加,却因传入字符串导致隐式类型转换,结果出乎意料。这类“二义性”错误在小型脚本中或许无伤大雅,但在复杂业务逻辑中,可能引发难以追踪的 bug。

TypeScript 的解法:编译期类型检查

TypeScript 通过类型注解,在代码编写和编译阶段就捕获潜在错误:

function addTs(a: number, b: number): number {
  return a + b;
}
const result2 = addTs(10, 10); // 正确
// addTs(10, '10'); // 编译报错:Argument of type 'string' is not assignable to parameter of type 'number'.

类型签名 a: number 明确约束了参数类型,编辑器会立即提示错误,无需等到运行时才发现问题。这种“早发现、早修复”的机制,极大提升了代码健壮性。

基础类型与类型推导

TS 提供丰富的内置类型,并支持类型自动推导:

let a: number = 10;
let b: string = 'hello';
let arr: number[] = [1, 2, 3];
let user: [number, string, boolean] = [1, '张三', true]; // 元组

即使省略显式注解,TS 也能根据初始值推断类型:

let count = 100; // 自动推导为 number
// count = '100'; // 错误!不能将 string 赋值给 number

这种“写得少,检查多”的体验,让开发者既能享受简洁语法,又不失类型安全。

接口与自定义类型:描述复杂结构

对于对象,TS 提供 interfacetype 来定义结构契约:

interface IUser {
  name: string;
  age: number;
  readonly id: number; // 只读属性
  hobby?: string;      // 可选属性
}

let user3: IUser = {
  name: '张三',
  age: 10,
  id: 1001
};
// user3.id = 1002; // 错误!id 是只读的

接口清晰表达了对象应具备的字段及其约束,不仅防止非法赋值,还为 IDE 提供精准的智能提示和文档查看能力。

联合类型与泛型:应对多样性与复用

TS 支持联合类型处理多可能性:

type ID = string | number;
let id: ID = 1001;
id = 'user_001'; // 合法

而泛型则实现类型级别的参数化,提升组件复用性:

let arr2: Array<string> = ['a', 'b', 'c'];
// 或简写为 string[]

泛型让函数、类、接口能适用于多种类型,同时保持类型安全,是构建通用库的核心工具。

安全的未知类型:unknown vs any

面对不确定的类型,TS 提供 unknown 作为更安全的替代方案:

let bb: unknown = 10;
bb = 'hello';
// bb.toUpperCase(); // 错误!需先类型检查
if (typeof bb === 'string') {
  console.log(bb.toUpperCase()); // 安全调用
}

相比之下,any 会完全绕过类型检查,虽可作为迁移旧代码的“救命稻草”,但应尽量避免在新项目中使用。

工程价值:不止于防错

TypeScript 的优势远超错误预防:

  • 智能提示:输入对象属性时自动补全;
  • 重构安全:重命名变量或函数时,所有引用同步更新;
  • 代码导航:一键跳转到类型定义或实现;
  • 文档内嵌:类型本身就是最好的文档;
  • 垃圾清理:未使用的变量、导入会高亮提示。

这些特性显著提升开发效率,尤其在团队协作和长期维护中,价值倍增。

用 TypeScript + React Hooks 构建一个健壮的 Todo 应用

最近我用 React 和 TypeScript 从零写了一个 Todo 应用,整个过程让我深刻体会到:类型系统不是束缚,而是保护。它让组件之间的协作更清晰,状态管理更可靠,连 localStorage 的读写都变得安全可控。下面分享我是怎么一步步搭建这个小项目的。

核心思路:状态集中 + 类型约束

我把所有 todo 数据和操作逻辑封装在一个自定义 Hook useTodos 里,这样组件只需要“消费”状态和方法,不用关心实现细节。同时,用 TypeScript 接口明确约定数据结构,避免传错参数或访问不存在的属性。

首先定义 Todo 的结构:

// types/todo.ts
export interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

这个接口就像一份契约——任何地方使用 Todo 数据,都必须包含这三个字段,且类型固定。

自定义 Hook:useTodos

这是整个应用的核心。它用 useState 管理状态,并通过 useEffect 同步到 localStorage:

// hooks/useTodos.ts
export function useTodos() {
  const [todos, setTodos] = useState<Todo[]>(() => 
    getStorage<Todo[]>(STORAGE_KEY, [])
  );

  useEffect(() => {
    setStorage<Todo[]>(STORAGE_KEY, todos);
  }, [todos]);

  const addTodo = (title: string) => {
    const newTodo: Todo = {
      id: +new Date(),
      title,
      completed: false
    };
    setTodos([...todos, newTodo]);
  };

  // toggleTodo 和 removeTodo 略...

  return { todos, addTodo, toggleTodo, removeTodo };
}

注意这里用了泛型函数 getStorage<T>setStorage<T>,确保读写 localStorage 时类型安全:

// utils/storages.ts
export function getStorage<T>(key: string, defaultValue: T): T {
  const item = localStorage.getItem(key);
  return item ? JSON.parse(item) : defaultValue;
}

这样,即使从 localStorage 读出来的数据是字符串,TS 也能正确推断为 Todo[] 类型。

组件通信:靠 Props 接口对齐

父组件 App 只负责组合,不处理逻辑:

// App.tsx
export default function App() {
  const { todos, addTodo, toggleTodo, removeTodo } = useTodos();
  return (
    <div>
      <h1>TodoList</h1>
      <TodoInput onAdd={addTodo} />
      <TodoList todos={todos} onToggle={toggleTodo} onRemove={removeTodo} />
    </div>
  );
}

子组件通过 Props 接口明确声明自己需要什么:

// components/TodoList.tsx
interface Props {
  todos: Todo[];
  onToggle: (id: number) => void;
  onRemove: (id: number) => void;
}

const TodoList: React.FC<Props> = ({ todos, onToggle, onRemove }) => {
  return (
    <ul>
      {todos.map(todo => (
        <TodoItem key={todo.id} todo={todo} onToggle={onToggle} onRemove={onRemove} />
      ))}
    </ul>
  );
};

这样,如果我在 App 里不小心传了错误类型的 onToggle,TypeScript 会立刻报错,而不是等到运行时才发现按钮点不动。

单个 Todo 项:细节处理

TodoItem 中,根据 completed 状态动态设置样式:

<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
  {todo.title}
</span>

因为 todo 的类型是 Todo,所以 todo.completed 一定是布尔值,不会出现 undefined 导致样式异常。

输入框:防错处理

TodoInput 还做了空值校验:

const handleAdd = () => {
  if (!value.trim()) return; // 防止添加空任务
  onAdd(value);
  setValue('');
};

配合 TS 的 string 类型,确保传给 onAdd 的一定是字符串,不会意外传入数字或 null。

总结:为什么值得用 TS?

  • 提前暴露问题:写代码时就知道哪里传参错了;
  • 自动文档:鼠标悬停就能看到函数签名和字段说明;
  • 重构安全:改一个接口,所有用到的地方都会提示更新;
  • 团队协作友好:别人看你的组件,一眼就知道要传什么。

这个 Todo 应用虽然简单,但用 TS 写完后,感觉整个项目“稳”了很多。以后再做大项目,我肯定会首选 TypeScript —— 它不是增加负担,而是帮我们写出更干净、更可靠的代码。

Vue使用<Suspense/>实现图片加载组件

什么是Suspense?

SuspenseVue的内置组件,能让你管理组件加载时的等待、出错和最终渲染逻辑。

主要配合异步组件async/await 版的 setup() 使用。

它提供两个插槽:

  • default(默认插槽):要渲染的异步内容
  • fallback:等待异步内容加载时显示的兜底内容

话不多说,上代码:

第一步,实现图片异步组件

<template>
    <img 
    :src="props.src" 
    :alt="props.alt" 
    :style="{ 
      width: setWidth, 
      height: setHeight, 
      borderRadius: rounded, 
      objectFit: 'cover'
    }"
    />
</template>
// =================================== 此处为计算属性逻辑 ================================
<script setup lang="ts">
import { computed, ref } from "vue";

// 图片形状可选项
type ShapeOption = "circle" | "square" | "roundRect";

interface ILazyImage {
  src: string;
  width:string|number;
  height:string|number;
  delay?:number;//延迟执行?
  timeout?:number;//超时时间?
  shape?:ShapeOption;//图片形状
  alt?:string;
}

const props = defineProps<ILazyImage>();

//计算超时
const timeoutSet = computed(() => {
    let time = Number(props.timeout) || 5;
    return time * 1000;
});

//计算延迟
const delayTime = computed(() => {
    let time = Number(props.delay) || 0.1;
    return time * 1000;
});


//计算宽度
const setWidth = computed(() => {
    const val = parseFloat(props.width)
    if(!val) return '100px'
    return val  + "px";
});

//计算高度
const setHeight = computed(() => {
    const val = parseFloat(props.Height)
    if(!val) return '100px'
    return val  + "px";
});

// 计算圆角
const rounded = computed(() => {
    switch (props.shape) {
        case "circle":
            return "50%";
        case "square":
            return "0";
        case "roundRect":
            return "10px";
        default:
            return "10px";
    }
});

//========================实现异步加载图片==================================

const loadImage = (src: string) => {
    return new Promise((resolve, reject) => {
    // 创建一个AbortController,用于取消请求
        const controller = new AbortController();
        const signal = controller.signal;

        const timeoutId = setTimeout(() => {
        // 超时时取消请求
            controller.abort();
            reject(new Error("图片加载超时"));
        }, timeoutSet.value);

        setTimeout(() => {
            const image = new Image();
            image.src = src;
            image.onload = () => {
                clearTimeout(timeoutId);
                resolve(src);
            };
            image.onerror = () => {
                clearTimeout(timeoutId);
                reject(new Error("图片加载失败"));
            };
        }, delayTime.value);
    });
};

// 执行图片加载(async/await 触发 Suspense 等待)
await loadImage(props.src);

</script>

第二步:实现懒加载组件

  • default(默认插槽):要渲染的异步内容
  • fallback:等待异步内容加载时显示的兜底内容
  • 使用v-bind()绑定属性

<Suspense></Suspense>中直接使用异步组件即可,<template #fallback></template>则是加载时显示

<template>
    <div>
        <Suspense>
            <template #default>
                <div class="image">
                    <Image v-bind="{ ...props }"></Image>
                </div>
            </template>
            <template #fallback>
                <div class="skeleton"></div>
            </template>
        </Suspense>
    </div>
</template>

<script setup lang="ts">
import { computed } from "vue";
import Image from "./Image.vue";
// 图片形状可选项
type ShapeOption = "circle" | "square" | "roundRect";

interface ILazyImage {
  src: string;
  width:string|number;
  height:string|number;
  delay?:number;//延迟执行?
  timeout?:number;//超时时间?
  shape?:ShapeOption;//图片形状
  alt?:string;
}

const props = defineProps<ILazyImage>();

//计算超时
const timeoutSet = computed(() => {
    let time = Number(props.timeout) || 5;
    return time * 1000;
});

//计算延迟
const delayTime = computed(() => {
    let time = Number(props.delay) || 0.1;
    return time * 1000;
});


//计算宽度
const setWidth = computed(() => {
    const val = parseFloat(props.width)
    if(!val) return '100px'
    return val  + "px";
});

//计算高度
const setHeight = computed(() => {
    const val = parseFloat(props.Height)
    if(!val) return '100px'
    return val  + "px";
});

// 计算骨架屏动画:遮罩宽度(取容器宽度的 40%) 
const setSeletonWidth = computed(() => { 
    // 提取宽度数值(兼容 px 单位) 
    const widthNum = parseFloat(setWidth.value) || 100; 
    return `${widthNum * 0.4}px`;
}); 


// 骨架屏动画起始位置(从容器左侧外 30% 开始)
const setSeletonStartRight = computed(() => { 
    const widthNum = parseFloat(setWidth.value) || 100; 
    return `-${widthNum * 1.3}px`;  // 起始在容器左侧外
});

// 骨架屏动画结束位置(到容器右侧外 30%) 
const setSeletonEndRight = computed(() => { 
    const widthNum = parseFloat(setWidth.value) || 100; return `${widthNum * 1.3}px`;
    // 结束在容器右侧外
});

// 计算圆角
const rounded = computed(() => {
    switch (props.shape) {
        case "circle":
            return "50%";
        case "square":
            return "0";
        case "roundRect":
            return "10px";
        default:
            return "10px";
    }
});
</script>

<style scoped>
.image {
    overflow: hidden;
}

.skeleton {
    width: v-bind(setWidth);
    height: v-bind(setHeight);
    background-color: #dfe4ea;
    overflow: hidden;
    position: relative;

    border-radius: v-bind(rounded);
}

.skeleton::before {
    content: "";
    display: block;
    background-color: #ced6e0;
    box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
    position: absolute;
    top: 0;
    right: v-bind(setSeletonStartRight);
    width: v-bind(setSeletonWidth);
    height: v-bind(setHeight);
    transform: skewX(-30deg);
    animation: ping 1s infinite ease-out;
    filter: blur(10px);
}

@keyframes ping {
    from {
        right: v-bind(setSeletonStartRight);
    }

    to {
        right: v-bind(setSeletonEndRight);
    }
}
</style>

补充

  • 代码内有些重复的内容,可使用另外的ts来声明或实现。

  • 其中可能有些变量或实现过程有误,此文章仅供参考。

浏览器指纹管理:如何在 Electron 应用中实现多账号隔离

本文将详细介绍如何在 Electron 应用中实现浏览器指纹管理,涵盖技术选型、架构设计、核心实现到踩坑经验。适合对浏览器自动化、指纹技术感兴趣的开发者阅读。

为什么需要浏览器指纹管理

在 RPA(Robotic Process Automation)自动化场景中,我们经常需要同时管理多个账号。然而,现代网站已经发展出越来越复杂的反爬虫和反自动化检测机制:

🔍 常见的检测手段

检测类型 检测方式 风险等级
浏览器指纹 Canvas、WebGL、音频指纹等跨会话追踪 🔴 高
自动化检测 navigator.webdriver、CDP 痕迹检测 🔴 高
账号关联 通过指纹识别同一设备上的多个账号 🔴 高
会话污染 多账号共享 Cookie/LocalStorage 🟡 中

💡 业务痛点

同一台电脑 + 多个账号 = 账号关联风险
         ↓
网站检测到相同指纹
         ↓
账号被封禁/限制

我们的目标很明确:在同一台电脑上运行多个具有完全独立指纹的浏览器实例,每个实例看起来就像来自不同的设备。


技术选型:为什么选择定制 Chromium

方案对比

方案 优点 缺点 结论
Puppeteer Stealth 易集成、轻量 指纹修改有限、易被检测 ❌ 不够彻底
Playwright 跨浏览器、功能丰富 无原生指纹修改能力 ❌ 能力不足
fingerprint-chromium 深度指纹修改、开源免费 需定制浏览器 ✅ 最佳选择
商业指纹浏览器 功能完善 成本高、依赖第三方 ❌ 成本考量

选择 fingerprint-chromium 的理由

fingerprint-chromium 是基于 Ungoogled Chromium 的定制版本,它在浏览器底层实现了指纹修改能力:

  1. Canvas/WebGL/音频指纹自动修改 - 无需 JavaScript 注入
  2. navigator.webdriver 自动隐藏 - 内置反自动化检测
  3. CDP 检测规避 - 调用 Runtime.enable 不触发检测
  4. 通过命令行参数配置 - 灵活的指纹定制能力

系统架构设计

整体架构

┌──────────────────────────────────────────────────────────────────────┐
│                           RPA 工作流引擎                               │
│                    (任务调度、步骤执行、状态管理)                       │
└──────────────────────────────────┬───────────────────────────────────┘
                                   │
                                   ▼
┌──────────────────────────────────────────────────────────────────────┐
│                          BrowserManager                              │
│  ┌────────────────────────────────────────────────────────────────┐  │
│  │  • 浏览器实例生命周期管理                                          │  │
│  │  • 多实例协调与资源控制                                            │  │
│  │  • Storage State 持久化(登录状态保存)                            │  │
│  │  • 指纹配置解析与覆盖                                             │  │
│  └────────────────────────────────────────────────────────────────┘  │
└──────────────────────────────────┬───────────────────────────────────┘
                                   │
                                   ▼
┌──────────────────────────────────────────────────────────────────────┐
│                      LocalFingerprintService                         │
│  ┌────────────────────────────────────────────────────────────────┐  │
│  │  • 指纹生成(基于种子的确定性算法)                                  │  │
│  │  • Chromium 进程管理                                             │  │
│  │  • CDP 连接管理                                                  │  │
│  │  • 端口动态分配                                                  │  │
│  └────────────────────────────────────────────────────────────────┘  │
└──────────────────────────────────┬───────────────────────────────────┘
                                   │
                                   ▼
┌──────────────────────────────────────────────────────────────────────┐
│                    Fingerprint Chromium (定制浏览器)                   │
│  ┌────────────────────────────────────────────────────────────────┐  │
│  │  • 通过命令行参数接收指纹配置                                      │  │
│  │  • Canvas/WebGL/音频指纹自动修改                                  │  │
│  │  • navigator.webdriver 隐藏                                     │  │
│  │  • CDP 检测规避                                                 │  │
│  └────────────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────────────┘

核心模块职责

模块 职责 关键能力
BrowserManager 高层封装 生命周期管理、配置解析、会话持久化
LocalFingerprintService 核心实现 指纹生成、进程管理、端口分配
FingerprintConfigValidator 配置验证 参数校验、GPU/平台匹配、版本迁移

核心实现详解

1. 指纹种子机制

我们使用种子(Seed)机制确保指纹的确定性和一致性:

// 基于 profileId 生成确定性种子
const fingerprintSeed = Math.abs(hashCode(profileId)) % 2147483647

// 使用种子确定性选择各项配置
const fingerprint = {
  userAgent: userAgents[fingerprintSeed % userAgents.length],
  viewport: viewports[fingerprintSeed % viewports.length],
  language: languages[fingerprintSeed % languages.length],
  timezone: timezones[fingerprintSeed % timezones.length],
  gpu: gpuConfigs[fingerprintSeed % gpuConfigs.length],
  cores: coreOptions[fingerprintSeed % coreOptions.length]
}

优点

  • ✅ 相同 profileId 始终生成相同指纹
  • ✅ 支持自定义种子实现跨设备一致性
  • ✅ 可通过配置覆盖任意指纹项

2. 指纹覆盖机制

用户可以通过配置覆盖自动生成的任意指纹项:

// BrowserManager._extractFingerprintOverrides()
_extractFingerprintOverrides(rawConfig) {
  const cfg = this._normalizeFingerprintConfig(rawConfig)
  const overrides = { rawConfig: cfg }

  // User Agent 配置
  if (cfg.uaMode === 'custom' && cfg.userAgent?.trim()) {
    overrides.userAgent = cfg.userAgent.trim()
  }

  // 语言配置(非自动模式时生效)
  if (cfg.languageAuto === false && cfg.language?.trim()) {
    overrides.language = cfg.language.trim()
  }

  // 时区配置
  if (cfg.timezoneAuto === false && cfg.timezone?.trim()) {
    overrides.timezone = cfg.timezone.trim()
  }

  // GPU 配置覆盖
  if (cfg.gpuVendor || cfg.gpuRenderer) {
    overrides.gpuVendor = cfg.gpuVendor
    overrides.gpuRenderer = cfg.gpuRenderer
  }

  // CPU 核心数覆盖
  if (cfg.hardwareConcurrency > 0) {
    overrides.hardwareConcurrency = cfg.hardwareConcurrency
  }

  return overrides
}

3. 动态 User-Agent 生成

根据平台、品牌、版本动态生成一致的 User-Agent:

// LocalFingerprintService._generateUserAgent()
_generateUserAgent(platform, brand, brandVersion, platformVersion) {
  // Windows NT 版本映射
  const ntVersion = WINDOWS_NT_VERSION_MAP[platformVersion] || '10.0'

  // 基础 UA 模板
  let ua
  if (platform === 'windows') {
    ua = `Mozilla/5.0 (Windows NT ${ntVersion}; Win64; x64) ` +
         `AppleWebKit/537.36 (KHTML, like Gecko) ` +
         `Chrome/${brandVersion} Safari/537.36`
  } else if (platform === 'macos') {
    ua = `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ` +
         `AppleWebKit/537.36 (KHTML, like Gecko) ` +
         `Chrome/${brandVersion} Safari/537.36`
  }

  // 添加品牌后缀
  if (brand === 'Edge') {
    ua += ` Edg/${brandVersion}`
  } else if (brand === 'Opera') {
    ua += ` OPR/${brandVersion}`
  }

  return ua
}

4. 命令行参数构建

最终生成的指纹通过命令行参数传递给 Chromium:

// LocalFingerprintService.launch()
const chromiumArgs = [
  // 调试端口
  `--remote-debugging-port=${debuggingPort}`,

  // 用户数据目录(实现会话隔离)
  `--user-data-dir=${userDataDir}`,

  // 指纹核心参数
  `--fingerprint=${fingerprintSeed}`,
  `--fingerprint-platform=${platform}`,
  `--fingerprint-platform-version=${platformVersion}`,
  `--fingerprint-brand=${brand}`,
  `--fingerprint-brand-version=${brandVersion}`,
  `--fingerprint-hardware-concurrency=${cores}`,
  `--fingerprint-gpu-vendor=${gpuVendor}`,
  `--fingerprint-gpu-renderer=${gpuRenderer}`,

  // 基本设置
  `--lang=${language}`,
  `--accept-lang=${acceptLanguage}`,
  `--timezone=${timezone}`,
  `--user-agent=${userAgent}`,

  // 窗口设置
  `--window-size=${viewport.width},${viewport.height}`,

  // 代理设置(可选)
  ...(proxyServer ? [`--proxy-server=${proxyServer}`] : [])
]

指纹配置能力矩阵

可配置的指纹类型

指纹类型 命令行参数 配置方式 说明
指纹种子 --fingerprint 自动/手动 核心参数,启用后大部分指纹功能生效
User Agent --user-agent 自动/手动 修改 navigator.userAgent 及相关 API
操作系统 --fingerprint-platform 手动 windows / linux / macos
平台版本 --fingerprint-platform-version 自动/手动 如 10.0 (Win10)、14.0 (macOS 14)
浏览器品牌 --fingerprint-brand 自动/手动 Chrome / Edge / Opera / Vivaldi / Brave
浏览器版本 --fingerprint-brand-version 自动/手动 如 139.0.0.0
CPU核心数 --fingerprint-hardware-concurrency 自动/手动 2 / 4 / 6 / 8 / 12 / 16
GPU厂商 --fingerprint-gpu-vendor 自动/手动 NVIDIA / AMD / Intel / Apple
GPU渲染器 --fingerprint-gpu-renderer 自动/手动 具体 GPU 型号
语言 --lang 自动/手动 zh-CN / en-US 等
时区 --timezone 自动/手动 Asia/Shanghai 等
代理服务器 --proxy-server 手动 支持 HTTP/SOCKS5 协议

自动处理的指纹

以下指纹由 fingerprint-chromium 自动处理,无需配置:

指纹类型 说明
Canvas 图像 自动修改 Canvas 2D 渲染输出
WebGL 图像 自动修改 WebGL 渲染输出
音频指纹 自动修改 AudioContext 输出
字体指纹 修改系统字体列表
ClientRects 修改元素边界矩形
WebRTC 修改 WebRTC 相关指纹

实战:从配置到启动的完整流程

流程图

┌─────────────┐    ┌─────────────────┐    ┌───────────────────┐
│  前端配置页   │───>│ BrowserManager  │───>│ LocalFingerprint  │
│  (Vue 组件)  │    │  .openBrowser() │    │   Service.launch()│
└─────────────┘    └────────┬────────┘    └─────────┬─────────┘
                           │                       │
                           ▼                       ▼
                  ┌─────────────────┐    ┌───────────────────┐
                  │ 解析指纹配置      │    │ 生成指纹参数        │
                  │ 提取覆盖值        │    │ 构建命令行参数      │
                  └────────┬────────┘    └─────────┬─────────┘
                           │                       │
                           ▼                       ▼
                  ┌─────────────────┐    ┌───────────────────┐
                  │ 加载 StorageState│   │ 启动 Chromium 进程  │
                  │ (恢复登录状态)    │    │ 等待端口就绪        │
                  └────────┬────────┘    └─────────┬─────────┘
                           │                       │
                           ▼                       ▼
                  ┌─────────────────────────────────────────┐
                  │     CDP 连接建立,返回 page 对象           │
                  └─────────────────────────────────────────┘

代码示例:打开浏览器

// 1. 调用 BrowserManager 打开浏览器
const { browser, context, page } = await browserManager.openBrowser({
  profileId: 'profile-1',
  fingerprintConfig: {
    os: 'win10',
    uaMode: 'auto',
    languageAuto: false,
    language: 'en-US',
    gpuAuto: false,
    gpuVendor: 'NVIDIA Corporation',
    gpuRenderer: 'NVIDIA GeForce RTX 3060'
  }
})

// 2. 使用页面
await page.goto('https://example.com')

// 3. 保存登录状态(可选)
await browserManager.saveStorageState('profile-1', browserId)

// 4. 关闭浏览器
await browserManager.closeBrowser(browserId)

踩坑记录与解决方案

踩坑 1:平台覆盖不生效

问题:配置 os: "win8" 后,浏览器仍使用本机平台 (macOS)。

原因分析

  • BrowserManager 未正确提取 platform 字段
  • LocalFingerprintService 只使用了 process.platform

解决方案

// 添加 os 到 platform 的映射
_mapPlatform(osCode) {
  const mapping = {
    win7: 'windows', win8: 'windows', win10: 'windows', win11: 'windows',
    mac13: 'macos', mac14: 'macos', mac15: 'macos',
    linux: 'linux'
  }
  return mapping[osCode] || process.platform
}

// 修改 launch() 中调用顺序,先计算 targetPlatform
const targetPlatform = fingerprintOverrides?.platform ||
  this._mapPlatform(fingerprintOverrides?.rawConfig?.os)

踩坑 2:User-Agent 与品牌不一致

问题:配置 Edge 品牌时,User-Agent 仍为 Chrome 格式。

原因generateFingerprint() 使用静态 User-Agent 列表,不考虑品牌配置。

解决方案:新增 _generateUserAgent() 方法,根据平台、品牌、版本动态生成。

踩坑 3:GPU 与平台不匹配

问题:用户可能配置不匹配的 GPU(如 Windows 平台配置 Apple GPU)。

解决方案:实现 GPU/平台自动修复:

// fingerprintConfigValidator.js
function autoFixGpuPlatformMismatch(config) {
  const platform = mapOsToPlatform(config.os)
  const compatibility = GPU_PLATFORM_COMPATIBILITY[platform]

  // 检查 GPU 厂商是否与平台兼容
  if (!compatibility.vendors.includes(config.gpuVendor)) {
    return {
      ...config,
      gpuVendor: compatibility.defaultGpu.vendor,
      gpuRenderer: compatibility.defaultGpu.renderer,
      _autoFixed: true,
      _fixReason: `GPU 厂商 "${config.gpuVendor}" 与 ${platform} 平台不匹配`
    }
  }

  return config
}

踩坑 4:代理认证问题

问题:带用户名密码的代理 URL 无法正常工作。

解决方案:实现代理 URL 解析和认证处理:

// proxyAuthHelper.js
function parseProxyUrl(proxyUrl) {
  const url = new URL(proxyUrl)
  return {
    server: `${url.protocol}//${url.host}`,
    protocol: url.protocol.replace(':', ''),
    host: url.hostname,
    port: url.port,
    username: decodeURIComponent(url.username),
    password: decodeURIComponent(url.password)
  }
}

// 使用 CDP 设置代理认证
async function setupProxyAuth(context, proxyConfig) {
  if (proxyConfig.username && proxyConfig.password) {
    await context.route('**/*', async (route) => {
      await route.continue({
        headers: {
          'Proxy-Authorization': `Basic ${btoa(
            `${proxyConfig.username}:${proxyConfig.password}`
          )}`
        }
      })
    })
  }
}

性能优化与最佳实践

1. 低内存模式

const LOW_MEMORY_CONFIG = {
  lowMemoryMode: true,
  mediaCacheSizeMB: 128,
  mediaCacheDiskSizeMB: 512,
  enableVideoOptimizations: false
}

// 禁用重量级视频管道标志
const HEAVY_VIDEO_PIPELINE_FLAGS = [
  '--enable-av1-decoder',
  '--enable-video-decoding-multiple-threads',
  '--enable-features=VaapiVideoDecoder,PlatformVideoDecoder'
]

2. 会话隔离最佳实践

// 每个实例使用独立的用户数据目录
const userDataDir = path.join(
  profilesDir,
  `profile-${profileId}-${uniqueId}`
)

// 登录数据白名单(仅持久化必要文件)
const LOGIN_WHITELIST = [
  'Default/Cookies',
  'Default/Login Data',
  'Default/Local Storage',
  'Default/Session Storage'
]

3. 指纹一致性检查

// 确保指纹参数之间的一致性
function validateFingerprintConsistency(config) {
  const warnings = []

  // Windows 平台应使用 Windows GPU
  if (config.platform === 'windows' &&
      config.gpuVendor === 'Apple Inc.') {
    warnings.push('Windows 平台不应使用 Apple GPU')
  }

  // 语言和时区应地理位置一致
  if (config.language === 'zh-CN' &&
      config.timezone === 'America/New_York') {
    warnings.push('语言和时区地理位置不一致')
  }

  return { valid: warnings.length === 0, warnings }
}

4. 指纹验证

// 快速验证指纹是否生效
async function quickVerify(page) {
  const result = await page.evaluate(() => ({
    webdriver: navigator.webdriver,
    platform: navigator.platform,
    userAgent: navigator.userAgent,
    hardwareConcurrency: navigator.hardwareConcurrency,
    languages: navigator.languages
  }))

  return {
    passed: result.webdriver === false,
    details: result
  }
}

总结

核心能力回顾

通过本文介绍的方案,我们实现了以下能力:

能力 说明
多指纹实例 同时运行多个具有独立指纹的浏览器
指纹自定义 支持自定义 UA、GPU、CPU、时区、平台等
会话隔离 每个实例独立的 Cookie 和 LocalStorage
指纹一致性 相同配置 ID 生成相同指纹
登录持久化 Storage State 机制保存登录状态
配置验证 自动验证和修复配置问题

技术要点总结

  1. 种子机制 是实现指纹确定性和一致性的关键
  2. 分层架构(BrowserManager → LocalFingerprintService → Chromium)便于维护和扩展
  3. 配置覆盖机制 允许用户灵活定制任意指纹项
  4. GPU/平台匹配验证 可以避免不合理的配置组合
  5. 代理认证处理 需要单独解析 URL 并设置认证头

延伸阅读

如果你对浏览器指纹技术感兴趣,可以进一步了解:

深入浅出 LSD:基于 Solidity 0.8.24 与 OpenZeppelin V5 构建流动性质押协议

前言

本文主要梳理流动性质押协议的理论知识,涵盖概念机制、核心功能、行业痛点及应用优劣势分析。同时,将借助 Solidity 0.8.24 与 OpenZeppelin V5,从 0 到 1 实现一个流动性质押协议,完整展示开发、测试与部署的全流程。

流动性质押(LSD)理论梳理

一、主要概念与核心机制

定义:PoS链中,用户质押ETH、SOL等原生资产至协议,智能合约即时发放1:1挂钩的LST(流动性质押代币),代表质押权益与收益,LST可在DeFi生态自由流转,实现“质押不锁仓”。

核心流程:用户存入资产→协议聚合资金并委托验证节点挖矿→发放LST(价值随收益增长)→用户可交易、使用LST或赎回原生资产(部分有锁定期/滑点)。

关键角色:协议层(智能合约管控资产与分配)、验证节点(保障网络安全与收益)、用户(获取LST兼顾流动性与收益)。

二、核心功能

  • 质押与流动性兼得,LST可参与DeFi实现收益叠加;
  • 降低门槛,小额资金可通过协议聚合参与,无需自建节点;
  • 资产复用增值,LST可借贷抵押、DEX提供流动性,或参与再质押提升年化;
  • 灵活退出,通过二级市场交易或协议赎回快速止损。

三、解决的行业痛点

痛点 传统质押 流动性质押解决方案
流动性缺失 资产长期锁定,无法再投资 LST可自由交易、借贷,释放流动性
高门槛 需大额资金+技术运维能力 一键质押,协议聚合小额资金,零技术要求
资本效率低 仅获基础质押收益,机会成本高 同一资产叠加质押与DeFi双重收益
退出困难 解锁周期长,难以及时止损 二级市场即时交易或快速赎回通道
中心化风险 节点运营商垄断,去中心化不足 多节点竞争,用户可自选节点,分散权力

四、行业应用场景

  1. 去中心化借贷:LST作为抵押品借款,实现“质押+借贷”杠杆;
  2. DEX流动性:提供LST/原生资产交易对,赚取手续费与挖矿奖励;
  3. 再质押:存入EigenLayer等平台,年化收益可从8%提升至15%-20%;
  4. 资产管理:构建低风险、高流动性理财组合;
  5. 衍生品生态:基于LST发行稳定币或开发期货、期权。

五、优势与劣势分析

优势:资本效率高,收益叠加;门槛低、操作便捷;退出灵活;助力网络去中心化。

劣势:存在智能合约安全风险;极端市场下LST可能与原生资产脱钩;头部协议有节点集中隐患;部分协议赎回受限;LST监管性质尚不明确。

智能合约开发、测试、部署

智能合约

流动性质押智能合约:它的核心逻辑是:用户质押 ETH,合约按 1:1 的比例铸造代币(stETH)给用户;当用户销毁 stETH 时,返还等量的 ETH

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

/**
 * @title SimpleLiquidStaking
 * @dev 实现基础的 ETH 质押并获得 LSD 代币 (stETH)
 */
contract SimpleLiquidStaking is ERC20, Ownable, ReentrancyGuard {
    
    event Staked(address indexed user, uint256 amount);
    event Withdrawn(address indexed user, uint256 amount);

    // 初始化时设置代币名称和符号,并将所有权移交给部署者
    constructor() ERC20("Liquid Staked ETH", "stETH") Ownable(msg.sender) {}

    /**
     * @notice 用户质押 ETH,获得等额 stETH
     * @dev 使用 nonReentrant 防止重入攻击
     */
    function stake() external payable nonReentrant {
        require(msg.value > 0, "Amount must be greater than 0");
        
        // 1:1 铸造代币给用户
        _mint(msg.sender, msg.value);
        
        emit Staked(msg.sender, msg.value);
    }

    /**
     * @notice 用户销毁 stETH,取回等额 ETH
     * @param amount 想要提取的金额
     */
    function withdraw(uint256 amount) external nonReentrant {
        require(amount > 0, "Amount must be greater than 0");
        require(balanceOf(msg.sender) >= amount, "Insufficient stETH balance");

        // 先销毁用户的 stETH 凭证
        _burn(msg.sender, amount);
        
        // 发送 ETH 给用户
        (bool success, ) = payable(msg.sender).call{value: amount}("");
        require(success, "ETH transfer failed");

        emit Withdrawn(msg.sender, amount);
    }

    /**
     * @dev 允许合约接收 ETH (例如验证者节点的奖励返还)
     */
    receive() external payable {}
}

部署脚本

// scripts/deploy.js
import { network, artifacts } from "hardhat";
import { parseUnits } from "viem";
async function main() {
  // 连接网络
  const { viem } = await network.connect({ network: network.name });//指定网络进行链接
  
  // 获取客户端
  const [deployer, investor] = await viem.getWalletClients();
  const publicClient = await viem.getPublicClient();
 
  const deployerAddress = deployer.account.address;
   console.log("部署者的地址:", deployerAddress);
  
  // 部署SimpleLiquidStaking合约
  const SimpleLiquidStakingArtifact = await artifacts.readArtifact("SimpleLiquidStaking");
  // 1. 部署合约并获取交易哈希
  const SimpleLiquidStakingHash = await deployer.deployContract({
    abi: SimpleLiquidStakingArtifact.abi,
    bytecode: SimpleLiquidStakingArtifact.bytecode,
    args: [],
  });
  const SimpleLiquidStakingReceipt = await publicClient.waitForTransactionReceipt({ 
     hash: SimpleLiquidStakingHash 
   });
   console.log("SimpleLiquidStaking合约地址:", SimpleLiquidStakingReceipt.contractAddress);
}

main().catch(console.error);

测试脚本

import assert from "node:assert/strict";
import { describe, it, beforeEach } from "node:test";
import { parseEther } from 'viem';
// 注意:在 Hardhat 环境中通常通过 hre 获取 viem
import hre from "hardhat";

describe("SimpleLiquidStaking 测试", async function() {
    const { viem } = await hre.network.connect();
    let simpleLiquidStaking: any;
    let publicClient: any;
    let owner: any, user1: any;
    let deployerAddress: string;

    beforeEach(async function () {
        
        // 获取 clients
        publicClient = await viem.getPublicClient();
        [owner, user1] = await viem.getWalletClients();
        deployerAddress = owner.account.address;

        // 部署合约,注意 OpenZeppelin V5 的 Ownable 需要构造参数(可选,取决于你合约具体实现)
        // 如果你的合约构造函数是 constructor() ERC20(...) Ownable(msg.sender) {}
        simpleLiquidStaking = await viem.deployContract("SimpleLiquidStaking", []);
    });

    it("用户应该能够成功质押 ETH 并获得 stETH", async function () {
        const stakeAmount = parseEther("10");

        // 1. 执行质押操作
        const hash = await user1.writeContract({
            address: simpleLiquidStaking.address,
            abi: simpleLiquidStaking.abi,
            functionName: "stake",
            value: stakeAmount,
        });

        // 2. 等待交易确认
        await publicClient.waitForTransactionReceipt({ hash });

        // 3. 验证合约中的 ETH 余额
        const contractEthBalance = await publicClient.getBalance({
            address: simpleLiquidStaking.address,
        });
        assert.equal(contractEthBalance, stakeAmount, "合约收到的 ETH 数量不正确");

        // 4. 验证用户收到的 stETH 凭证数量
        const userStEthBalance = await simpleLiquidStaking.read.balanceOf([user1.account.address]);
        assert.equal(userStEthBalance, stakeAmount, "用户获得的 stETH 数量不正确");
    });

    it("用户应该能够通过销毁 stETH 赎回 ETH", async function () {
        const amount = parseEther("5");

        // 1. 先质押
        await user1.writeContract({
            address: simpleLiquidStaking.address,
            abi: simpleLiquidStaking.abi,
            functionName: "stake",
            value: amount,
        });

        const ethBalanceBefore = await publicClient.getBalance({ address: user1.account.address });

        // 2. 执行赎回
        const hash = await user1.writeContract({
            address: simpleLiquidStaking.address,
            abi: simpleLiquidStaking.abi,
            functionName: "withdraw",
            args: [amount],
        });
        const receipt = await publicClient.waitForTransactionReceipt({ hash });
        // console.log(receipt);
        // 3. 验证 stETH 是否已销毁
        const stEthBalanceAfter = await simpleLiquidStaking.read.balanceOf([user1.account.address]);
        assert.equal(stEthBalanceAfter, 0n, "stETH 应该已被销毁");

        // 4. 验证用户 ETH 余额是否增加 (考虑 Gas 费,余额应大于赎回前)
        const ethBalanceAfter = await publicClient.getBalance({ address: user1.account.address });
        assert.ok(ethBalanceAfter > ethBalanceBefore, "用户未收到赎回的 ETH");
    }); 
});

结语

至此,流动性质押协议的理论梳理与代码实现已全部落地。本文既夯实了理论认知,又通过Solidity 0.8.24与OpenZeppelin V5完成了从开发到部署的全流程实践,形成理论与实操的闭环。 本次落地案例为相关开发者提供了可参考的实践范式,也印证了流动性质押在区块链生态中的应用价值。未来可基于此进一步优化协议性能、应对行业挑战,助力DeFi生态持续迭代。

Next.js:颠覆传统的前端开发框架,为什么它如此受欢迎?

Next.js:颠覆传统的前端开发框架,为什么它如此受欢迎?

image.png

一、Next.js到底是什么?

Next.js 是一个基于 React 的全栈框架,由Vercel公司开发并维护。它不是简单的UI库,而是一个完整的Web应用开发解决方案,让你能够轻松构建高性能的React应用程序。

简单来说,Next.js = React + 路由系统 + 服务端渲染 + 构建优化 + API路由 + 更多开箱即用的功能。

二、Next.js的八大核心特点

1. 服务端渲染(SSR)与静态站点生成(SSG)

这是Next.js最亮眼的特性之一!传统React应用只在客户端渲染,而Next.js支持:

  • • 服务端渲染:页面在服务器上生成HTML,然后发送给客户端
  • • 静态生成:构建时预渲染页面,适合内容变化不大的页面
  • • 增量静态再生:保持静态页面的优势,同时支持内容更新
// 简单的静态页面生成示例
export async function getStaticProps() {
  const data = await fetch('https://api.example.com/data');
  return {
    props: { data },
  };
}

2. 文件系统路由

不需要复杂配置,直接在pages目录下创建文件即可定义路由:

pages/
  index.js        →  /
  about.js        →  /about
  blog/
    [slug].js     →  /blog/:slug (动态路由)

3. API路由

无需单独的后端服务器,在pages/api目录下创建文件即可编写API接口:

// pages/api/user.js
export default function handler(req, res) {
  res.status(200).json({ name'John Doe' });
}

4. 内置CSS和Sass支持

支持CSS Modules、Styled JSX、Sass等,开箱即用,无需额外配置。

5. 自动代码分割

Next.js会自动将代码拆分成小块,只加载当前页面需要的代码,大幅提升首屏加载速度。

6. 图像优化组件

<Image />组件自动优化图片:

  • • 自动调整尺寸
  • • 转换为现代格式(WebP)
  • • 延迟加载
  • • 防止布局偏移

7. TypeScript原生支持

只需创建一个tsconfig.json文件,Next.js会自动配置TypeScript支持。

8. 快速刷新(Fast Refresh)

开发模式下,保存文件即可实时看到更改,保持组件状态不变。

三、Next.js的五大优势

1. 极致的性能优化

Next.js的预渲染策略显著改善了:

  • • 首屏加载时间:服务器返回完整的HTML,用户立即看到内容
  • • SEO优化:搜索引擎可以轻松抓取页面内容
  • • 核心Web指标:在LCP、FID、CLS等关键指标上表现优异

2. 开发体验极佳

  • • 零配置起步
  • • 丰富的插件生态系统
  • • 优秀的错误报告和调试工具
  • • 完整的文档和活跃的社区

3. 全栈能力

从前端到后端API,一个框架搞定所有:

  • • 前端页面渲染
  • • API接口开发
  • • 中间件处理
  • • 数据库连接

4. 强大的部署能力

与Vercel平台无缝集成:

  • • 一键部署
  • • 自动HTTPS
  • • 全球CDN
  • • 预览部署
  • • 自动性能优化

5. 企业级特性

  • • 国际化路由
  • • 按需分析
  • • 预览模式
  • • 中间件支持
  • • 安全头部自动配置

四、Next.js适合哪些项目?

✅ 适合场景

  • • 需要SEO优化的内容型网站(博客、新闻、电商)
  • • 需要良好性能的Web应用
  • • 需要服务端渲染的应用程序
  • • 全栈项目,前后端统一技术栈

❌ 不太适合

  • • 纯静态的无交互页面(简单的静态站点生成器可能更轻量)
  • • 超大型企业应用(可能需要更定制的架构)

五、快速开始Next.js

创建Next.js项目只需要一行命令:

npx create-next-app@latest my-app
cd my-app
npm run dev

几秒钟后,你的开发服务器就会在 http://localhost:3000 启动!

六、成功案例

许多知名公司都在使用Next.js:

  • • Twitch - 直播平台
  • • Netflix - 部分页面
  • • TikTok - 官网
  • • Notion - 文档工具
  • • Hulu - 视频流媒体

七、学习资源推荐

  1. 1. 官方文档(强烈推荐):nextjs.org/docs
  2. 2. Next.js学习课程:官方免费互动教程
  3. 3. GitHub示例:官方示例仓库
  4. 4. 社区:GitHub Discussions、Reddit、Discord

结语

Next.js正在改变我们构建Web应用的方式。它将React的灵活性与生产就绪的功能相结合,提供了一个既适合初学者入门,又能满足企业级需求的完整解决方案。

无论你是想提升现有项目的性能,还是开始一个新项目,Next.js都值得你深入了解和尝试。

Post List、mockjs与axios实战学习笔记

Post List、mockjs与axios实战学习笔记

在现代前端开发中,数据请求、模拟数据与状态管理是核心环节。本文基于React+Vite技术栈,结合实战代码,系统梳理Post List数据渲染、axios请求封装、mockjs模拟接口三大模块的相关知识,剖析其技术原理、实现逻辑与开发规范,为前端项目的数据层搭建提供参考。

一、整体技术背景与核心流程

本文实战场景为移动端React项目的首页帖子列表(Post List)功能,核心目标是实现“前端请求-模拟数据-状态管理-页面渲染”的完整闭环。在前后端分离架构下,后端接口开发往往滞后于前端页面开发,此时需通过mockjs模拟接口返回数据,同时借助axios封装统一的请求逻辑,再通过Zustand管理全局状态,最终将数据渲染至页面。

核心技术栈:React 18+Vite 5+TypeScript+axios+mockjs+Zustand+TailwindCSS,各技术分工如下:

  • axios:负责发起HTTP请求,处理请求拦截、响应拦截、错误捕获等逻辑;
  • mockjs:在开发环境模拟后端接口,生成随机测试数据,实现前端独立开发;
  • Zustand:轻量级全局状态管理库,存储帖子列表数据与加载方法,实现组件间数据共享;
  • Vite:通过插件集成mock服务,配置路径别名,优化开发体验;
  • TypeScript:定义接口类型(Post、User),实现类型安全,避免数据异常。

完整数据流转流程:页面加载时触发useEffect调用loadMore方法 → Zustand调用封装好的fetchPosts接口 → axios发起GET请求 → mockjs拦截请求并返回模拟数据 → axios接收响应并处理 → Zustand更新posts状态 → 页面从状态中读取数据并渲染。

二、axios:HTTP请求封装与实战

2.1 axios核心特性

axios是一款基于Promise的HTTP客户端,支持浏览器端与Node.js环境,具备以下核心优势:

  • 支持请求/响应拦截器,可统一处理请求头、token验证、错误提示等;
  • 自动转换JSON数据,无需手动解析响应体;
  • 支持取消请求、超时设置、请求重试等高级功能;
  • 兼容性良好,可适配不同浏览器与Node.js版本;
  • 支持TypeScript类型推导,与TS项目无缝集成。

2.2 基础配置与封装规范

在实际项目中,需对axios进行统一封装,避免重复代码,便于维护。核心封装要点包括:基础路径设置、请求头配置、错误统一处理、类型定义等。

2.2.1 基础配置实现

代码中对axios的基础配置如下,位于src/api/config.ts(或对应文件):

import axios from 'axios';
// 接口地址都以/api开始
axios.defaults.baseURL = 'http://localhost:5173/api'
// 生产环境可切换为真实后端地址
// axios.defaults.baseURL = 'http://douyin.com:5173/api'

export default axios;

关键配置说明:

  • baseURL:设置请求基础路径,后续请求URL可省略基础部分,简化代码。开发环境指向本地Vite服务(配合mockjs),生产环境切换为后端真实接口地址;
  • 可扩展配置:如设置超时时间(timeout: 5000)、默认请求头(headers: {'Content-Type': 'application/json'})等。

2.2.2 接口函数封装

针对具体业务接口,封装独立的请求函数,便于复用与维护。以帖子列表请求为例,代码位于src/api/posts.ts

import axios from './config';
import type { Post } from '@/types';

export const fetchPosts = async (
    page: number = 1,
    limit: number = 10,
) => {
    try {
        const response = await axios.get('/posts', {
            params: {
                page,
                limit,
            }
        });
        console.log('获取帖子列表成功', response);
        return response.data;
    } catch (error) {
        console.error('获取帖子列表失败', error);
        throw error;
    }
};

封装要点与最佳实践:

  • TypeScript类型约束:导入Post类型,明确返回数据结构,实现类型安全。参数page、limit设置默认值,避免调用时传参遗漏;
  • 异步处理:使用async/await语法,替代Promise.then(),代码更简洁易读;
  • 错误捕获:通过try/catch捕获请求异常,打印错误日志便于排查问题,同时通过throw error向上层抛出异常,由调用方决定后续处理(如提示用户);
  • 参数传递:GET请求通过params属性传递查询参数(page、limit),axios会自动将其拼接为URL查询字符串(如/api/posts?page=1&limit=10);POST请求可通过data属性传递请求体。

2.3 进阶扩展:请求/响应拦截器

实际项目中,需通过拦截器实现全局统一逻辑,例如请求时添加token、响应时统一处理错误状态码等。以下为常见拦截器配置示例,可集成到axios基础配置中:

// 请求拦截器
axios.interceptors.request.use(
    (config) => {
        // 给每个请求添加token
        const token = localStorage.getItem('token');
        if (token) {
            config.headers.Authorization = `Bearer ${token}`;
        }
        return config;
    },
    (error) => {
        // 请求发起前的错误处理(如参数验证失败)
        return Promise.reject(error);
    }
);

// 响应拦截器
axios.interceptors.response.use(
    (response) => {
        // 统一处理响应数据,只返回data部分
        return response.data;
    },
    (error) => {
        // 统一处理错误状态码
        const status = error.response?.status;
        switch (status) {
            case 401:
                // 未授权,跳转登录页
                window.location.href = '/login';
                break;
            case 404:
                console.error('接口不存在');
                break;
            case 500:
                console.error('服务器内部错误');
                break;
        }
        return Promise.reject(error);
    }
);

拦截器的核心价值在于“集中处理”,减少重复代码,提升项目可维护性。

三、mockjs:前端模拟接口与测试数据生成

3.1 mockjs的核心作用

在前后端分离开发模式中,前端开发常依赖后端接口,但后端接口开发、联调往往需要一定时间。此时mockjs可实现以下功能:

  • 模拟后端接口,拦截前端请求,返回自定义模拟数据,使前端无需等待后端接口完成即可独立开发;
  • 生成大量随机测试数据,覆盖不同场景(如分页、异常状态),便于测试页面渲染效果;
  • 与真实接口格式一致,开发完成后只需切换baseURL即可无缝对接后端,无需修改业务代码。

3.2 Vite集成mock服务

Vite通过vite-plugin-mock插件集成mock服务,实现开发环境下的接口模拟。配置步骤如下:

3.2.1 安装依赖

npm install mockjs vite-plugin-mock --save-dev

3.2.2 Vite配置文件修改

vite.config.ts中配置mock服务,指定mock文件路径:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
import path from 'path'
import { viteMockServe } from 'vite-plugin-mock'

export default defineConfig({
  plugins: [
    react(), 
    tailwindcss(), 
    viteMockServe({
      mockPath: 'mock', // 指定mock文件存放目录
      localEnabled: true, // 开发环境启用mock
      prodEnabled: false, // 生产环境禁用mock
    })
  ],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src'), // 路径别名,简化导入
    }
  }
});

关键配置说明:

  • mockPath:指定mock配置文件的存放目录(本文为项目根目录下的mock文件夹);
  • localEnabled:控制开发环境是否启用mock服务,设为true即可在开发时使用模拟接口;
  • prodEnabled:生产环境需禁用mock,避免模拟数据干扰真实接口。

3.3 mockjs语法与接口模拟实现

3.3.1 mock文件结构规范

在mock目录下创建posts.js文件,用于定义帖子列表接口的模拟规则。mockjs的核心语法包括“数据模板定义”与“接口配置”两部分。

3.3.2 数据模板定义:生成随机测试数据

mockjs通过特定语法生成随机数据,支持中文、数字、日期、图片等多种类型,核心语法如下:

  • '属性名|规则': 值:定义数据生成规则,如'list|45': []表示生成长度为45的list数组;
  • 占位符@xxx:生成随机数据,如@ctitle(8,20)生成8-20字的中文标题,@integer(1,30)生成1-30的随机整数;
  • 自定义函数:可通过函数返回动态数据,如tags字段通过() => Mock.Random.pick(tags,2)从标签数组中随机选取2个。

帖子列表模拟数据生成代码:

import Mock from 'mockjs'
const tags = ['前端','后端','职场','AI','副业','面经','算法'];
const posts = Mock.mock({
    'list|45':[ // 生成45条帖子数据
        {
            title: '@ctitle(8,20)', // 中文标题(8-20字)
            brief: '@ctitle(20,100)', // 中文摘要(20-100字)
            totalComments: '@integer(1,30)', // 评论数(1-30)
            totalLikes: '@integer(0,500)', // 点赞数(0-500)
            publishedAt: '@datetime("yyyy-MM-dd HH:mm:ss")', // 发布时间
            user: {
                id: '@integer(1,100)',
                name: '@cname()', // 中文姓名
                avatar: '@image(300x200)' // 随机图片(300x200)
            },
            tags: () => Mock.Random.pick(tags,2), // 随机2个标签
            thumbnail: '@image(300x200)', // 缩略图
            pics: [
                '@image(300x200)',
                '@image(300x200)',
                '@image(300x200)',
            ],
            id: '@integer(1,1000000)', // 唯一ID
        }
    ]
}).list; // 提取list数组

3.3.3 接口配置:拦截请求并返回数据

通过配置url、method、response,实现对指定接口的拦截与响应。核心逻辑包括请求参数解析、分页处理、响应格式返回:

export default [
    {
        url: '/api/posts', // 匹配前端请求的URL(需与axios请求路径一致)
        method: 'get', // 请求方法(GET/POST等)
        // response函数:处理请求并返回响应数据
        response: ({ query }, res) => {
            console.log(query); // 打印请求参数(page、limit)
            const { page = '1' , limit = '10' } = query;
            // 将字符串参数转换为数字(前端传参可能为字符串,需处理类型)
            const currentPage = Number(page, 10);
            const size = parseInt(limit, 10);
            
            // 参数合法性校验
            if(isNaN(currentPage) || isNaN(size) || currentPage < 1 || size < 1){
                return {
                    code: 400,
                    msg: 'Invalid page or pageSize',
                    data: null
                };
            }

            // 分页逻辑计算
            const total = posts.length; // 总数据量
            const start = (currentPage - 1) * size; // 起始索引
            const end = start + size; // 结束索引
            const paginatedData = posts.slice(start, end); // 截取当前页数据

            // 返回响应结果(与后端接口格式一致)
            return {
                code: 200,
                msg: 'success',
                items: paginatedData,
                pagination: {
                    current: currentPage,
                    limit: size,
                    total,
                    totalPages: Math.ceil(total / size), // 总页数
                }
            };
        }
    }
];

接口模拟关键要点:

  • URL匹配:url需与axios请求的URL完全一致(含baseURL前缀),确保请求被正确拦截;
  • 参数处理:GET请求参数从query中获取,需注意类型转换(前端传参可能为字符串,需转为数字),同时进行合法性校验,返回对应错误信息;
  • 分页逻辑:通过slice方法截取当前页数据,计算总页数,返回分页信息,便于前端实现分页加载;
  • 响应格式统一:与后端约定好响应格式(code、msg、data/items、pagination),确保切换真实接口时无需修改前端逻辑。

3.4 mockjs进阶用法扩展

  • 多种请求方法支持:可配置POST、PUT、DELETE等方法的接口,POST请求参数从body中获取(({ body }, res) => {});
  • 动态数据生成:可根据请求参数动态生成数据,如根据用户ID返回对应用户的帖子;
  • 异常场景模拟:除了正常响应,还可模拟401(未授权)、404(接口不存在)、500(服务器错误)等状态,测试前端错误处理逻辑。

四、Post List:数据状态管理与页面渲染

4.1 TypeScript类型定义

为实现类型安全,需定义Post、User接口,明确数据结构。代码位于src/types/index.ts

export interface User{
    id: number;
    name: string;
    avatar?: string; // 可选属性(?表示)
}

export interface Post{
    id: number;
    title: string;
    brief: string; // 简介
    publishedAt: string; // 发布时间
    totalLikes?: number; // 点赞数(可选)
    totalComments?: number; // 评论数(可选)
    user: User; // 关联User接口
    tags: string[]; // 标签数组
    thumbnail?: string; // 缩略图(可选)
    pics?: string[]; // 图片数组(可选)
}

类型定义要点:

  • 必填属性直接定义类型,可选属性添加?
  • 关联接口(如Post中的user属性关联User),实现数据结构的嵌套约束;
  • 所有接口类型需与mock数据、后端接口返回数据保持一致,避免类型不匹配错误。

4.2 Zustand全局状态管理

Zustand是一款轻量级状态管理库,相比Redux更简洁,无需Provider包裹,适合中小型项目。本文用其存储帖子列表数据、轮播图数据及加载方法。

4.2.1 状态定义与实现

代码位于src/store/home.ts

import { create } from "zustand";
import type { SlideData } from "@/components/SlideShow";
import type { Post } from "@/types";
import { fetchPosts } from "@/api/posts";

// 定义状态接口
interface HomeState {
    banners: SlideData[]; // 轮播图数据
    posts: Post[]; // 帖子列表数据
    loadMore: () => Promise<void>; // 加载更多方法(分页加载)
}

// 创建状态管理实例
export const useHomeStore = create<HomeState>((set) => ({
    // 初始轮播图数据
    banners: [{
      id: 1,
      title: "React 生态系统",
      image: "https://images.unsplash.com/photo-1633356122544-f134324a6cee?q=80&w=2070&auto=format&fit=crop",
    },
    {
      id: 2,
      title: "移动端开发最佳实践",
      image: "https://img.36krcdn.com/hsossms/20260114/v2_1ddcc36679304d3390dd9b8545eaa57f@5091053@ai_oswg1012730oswg1053oswg495_img_png~tplv-1marlgjv7f-ai-v3:600:400:600:400:q70.jpg?x-oss-process=image/format,webp",
    },
    {
      id: 3,
      title: "百度上线七猫漫剧,打的什么主意?",
      image: "https://img.36krcdn.com/hsossms/20260114/v2_8dc528b02ded4f73b29b7c1019f8963a@5091053@ai_oswg1137571oswg1053oswg495_img_png~tplv-1marlgjv7f-ai-v3:600:400:600:400:q70.jpg?x-oss-process=image/format,webp",
    }],
    // 初始帖子列表为空
    posts: [],
    // 加载更多方法(异步)
    loadMore: async () => {
      const { items } = await fetchPosts();
      // 更新状态:将新获取的帖子数据追加到posts中(分页加载逻辑)
      set((state) => ({ posts: [...state.posts, ...items] }));
      console.log(items);
    }
}));

状态管理核心逻辑:

  • 状态接口定义:通过HomeState接口约束状态的结构与类型,确保状态数据合规;
  • 初始状态:banners设置初始轮播图数据,posts初始为空数组;
  • 异步方法:loadMore为异步方法,调用fetchPosts获取帖子数据,通过set方法更新状态。set方法支持函数参数,可获取当前状态,实现数据追加(分页加载核心逻辑)。

4.3 页面渲染与数据联动

首页组件(src/pages/Home.tsx)从状态中读取数据,渲染轮播图、帖子列表,并在组件加载时触发数据加载。

import { useEffect } from "react";
import Header from "@/components/Header";
import SlideShow from "@/components/SlideShow";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { useHomeStore } from "@/store/home";

export default function Home() {
    // 从状态中解构数据与方法
    const { banners, posts, loadMore } = useHomeStore();
    
    // 组件挂载时触发加载更多(获取第一页数据)
    useEffect(() => {
        loadMore();
    }, []); // 空依赖数组,仅在组件挂载时执行一次
    
    return ( 
        <>
        <Header title="首页" showBackButton={true} />
        <div className="p-4 space-y-4 ">
            {/* 轮播图组件,传入banners数据 */}
            <SlideShow slides={banners} />
            {/* 欢迎卡片 */}
            <Card>
                <CardHeader>
                    <CardTitle>欢迎来到React Mobile</CardTitle>
                </CardHeader>
                <CardContent>
                    <p className="text-muted-foreground">这是内容区域</p>
                </CardContent>
            </Card>
            {/* 帖子列表网格布局 */}
            <div className="grid grid-cols-2 gap-4">
                {/* 遍历posts数据渲染帖子卡片(当前为占位,可替换为真实帖子组件) */}
                {posts.map((post) => (
                    <div key={post.id} className="h-32 bg-white rounded-lg shadow-sm flex items-center justify-center border ">
                        {post.title}
                    </div>
                ))}
                {/* 原占位数据,可删除,替换为真实数据渲染 */}
                {/* {[1,2,...,25].map((i,index) => (...))} */}
            </div>
        </div>
        </>
    );
}

页面渲染关键要点:

  • 状态订阅:通过useHomeStore钩子订阅状态,当posts、banners发生变化时,组件会自动重新渲染;
  • 数据加载时机:通过useEffect在组件挂载时调用loadMore,获取第一页帖子数据;
  • 列表渲染:使用map遍历posts数组渲染列表,需指定唯一key(post.id),避免React渲染警告。原代码中的占位数据可替换为真实帖子组件,展示帖子标题、缩略图等信息;
  • 样式与布局:通过TailwindCSS实现网格布局(grid-cols-2)、间距控制(space-y-4、gap-4),适配移动端展示。

五、核心技术整合与实战总结

5.1 技术整合关键点

  • 接口一致性:mock数据格式、TypeScript接口、后端接口文档三者必须保持一致,这是实现“无缝切换”的核心前提;
  • 分层设计:请求层(axios)、模拟数据层(mockjs)、状态层(Zustand)、视图层(页面组件)分层清晰,便于维护与扩展;
  • 类型安全:全程使用TypeScript定义类型,从请求参数、响应数据到状态管理、组件Props,避免数据异常导致的bug;
  • 开发效率:mockjs使前端独立开发,无需依赖后端,Vite插件集成简化配置,Zustand减少状态管理冗余代码,整体提升开发效率。

5.2 常见问题与解决方案

  • mock请求拦截失败:检查mock文件路径是否与vite.config.ts中mockPath配置一致,URL是否与axios请求路径完全匹配,确保localEnabled设为true;
  • 类型不匹配错误:检查TypeScript接口定义与mock数据、响应数据是否一致,确保可选属性、嵌套结构正确;
  • 分页逻辑异常:确认page、limit参数类型转换正确,分页计算公式(start = (currentPage-1)*size)无误,slice方法截取范围正确;
  • 状态更新后组件不渲染:确保通过Zustand的set方法更新状态,且组件正确订阅状态(使用useHomeStore钩子解构数据)。

5.3 生产环境部署注意事项

  • 切换axios的baseURL为后端真实接口地址,禁用mock服务(prodEnabled: false);
  • 完善错误处理逻辑,添加用户可感知的错误提示(如Toast组件),替代控制台打印;
  • 优化请求性能,如添加请求缓存、防抖节流(针对下拉加载更多)、超时重连等;
  • 校验后端接口返回数据,处理异常状态码,确保生产环境数据稳定性。

六、扩展学习与进阶方向

  • axios进阶:学习请求取消(如页面卸载时取消未完成请求)、请求重试、上传下载进度监控等高级功能;
  • mockjs扩展:使用mockjs结合JSON5语法编写更复杂的模拟规则,集成mock数据持久化(如localStorage);
  • 状态管理深化:学习Zustand的中间件(如日志、持久化),对比Redux、Pinia等状态管理库的适用场景;
  • 分页与无限滚动:基于当前分页逻辑,实现下拉加载更多、上拉刷新功能,集成第三方组件(如react-infinite-scroll-component);
  • 接口联调与测试:学习使用Postman、Swagger等工具测试后端接口,实现前端与后端的高效联调。

本文通过实战代码拆解,系统讲解了Post List功能开发中axios、mockjs的核心用法及状态管理、页面渲染的完整流程。掌握这些知识,可快速搭建前端项目的数据层架构,实现前后端分离模式下的高效开发。在实际项目中,需结合业务需求灵活扩展,不断优化代码质量与用户体验。

❌