阅读视图

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

ES6+ 新特性解析:让 JavaScript 开发更优雅高效

ES6(ECMAScript 2015)是 JavaScript 语言发展的里程碑,引入了大量让代码更简洁、更易维护的新特性。本文将深入解析这些特性,并通过实际代码示例展示它们的强大之处。

解构赋值:优雅的数据提取

解构赋值让我们能够从数组或对象中快速提取值,赋给变量。

数组解构

// 基本数组解构
const [a, b, c] = [1, 2, 3];
console.log(a, b, c); // 1 2 3

// 嵌套数组解构
const [a, [b, c, [d], e]] = [1, [2, 3, [4], 5]];
console.log(a, b, c, d, e); // 1 2 3 4 5

// 剩余参数解构
const arr = [1, 2, 3, 4, 5];
const [first, ...rest] = arr;
console.log(first, rest); // 1 [2, 3, 4, 5]

// 实际应用:提取教练和球员
const users = ['Darvin Ham', 'James', 'Luka', 'Davis', 'Ayton', 'Kyle'];
const [captain, ...players] = users;
console.log(captain, players); 
// Darvin Ham ['James', 'Luka', 'Davis', 'Ayton', 'Kyle']

对象解构

const sex = 'boy';
const obj = {
    name: 'Darvin Ham',
    age: 25,
    sex, // 对象属性简写
    like: {
        n: '唱跳'
    }
};

// 对象解构
let { name, age, like: { n } } = obj;
console.log(name, age, n); // Darvin Ham 25 唱跳

// 字符串也可以解构
const [e, r, ...u] = 'hello';
console.log(e, r, u); // h e ['l', 'l', 'o']

// 获取字符串长度
const { length } = 'hello';
console.log(length); // 5

解构的实用技巧

// 交换变量(无需临时变量)
let x = 1, y = 2;
[x, y] = [y, x];
console.log(x, y); // 2 1

// 函数参数解构
function greet({ name, greeting = 'Hello' }) {
    console.log(`${greeting}, ${name}!`);
}
greet({ name: 'Bob' }); // Hello, Bob!

// 设置默认值
const { name, gender = 'unknown' } = { name: 'Alice' };
console.log(gender); // unknown(因为 user 中没有 gender 属性)

模板字符串:更优雅的字符串处理

模板字符串使用反引号(``)定义,支持多行字符串和插值表达式。

let myName = 'zhangsan';

// 传统字符串拼接
console.log('hello, i am ' + myName);

// 模板字符串
console.log(`hello, i am ${myName}`);
console.log(`hello, i am ${myName.toUpperCase()}`);

// 多行字符串
const message = `
  亲爱的 ${myName}:
  欢迎使用我们的服务!
  祝您使用愉快!
`;
console.log(message);

更现代的循环:for...of

for...of 循环提供了更好的可读性和性能。

let myName = 'zhangsan';

// for...of 遍历字符串
for(let x of myName) {
    console.log(x); // 依次输出: z h a n g s a n
}

// 与 for 循环对比
const arr = [1, 2, 3, 4, 5];

// 传统的 for 循环
for(let i = 0; i < arr.length; i++) {
    console.log(arr[i]);
}

// 更简洁的 for...of
for(let item of arr) {
    console.log(item);
}

性能建议for...of 语义更好,可读性更强,性能也不会比计数循环差太多。而 for...in 性能较差,应尽量避免使用。

BigInt:处理大整数的新数据类型

JavaScript 的数字类型有精度限制,最大安全整数是 2^53-1。

// JavaScript 的数字精度问题
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991

let num = 1234567890987654321;
console.log(num); // 1234567890987654400(精度丢失!)

// 使用 BigInt
let num2 = 1234567890987654321n;
console.log(num2, typeof num2); // 1234567890987654321n 'bigint'

// BigInt 运算
const big1 = 9007199254740991n;
const big2 = 1n;
console.log(big1 + big2); // 9007199254740992n

// 注意事项
console.log(1n + 2); // ❌ TypeError: Cannot mix BigInt and other types
console.log(Math.sqrt(16n)); // ❌ TypeError
const x = 3.14n; // ❌ SyntaxError: Invalid or unexpected token

BigInt 使用要点:

  • 使用 n 后缀表示 BigInt 字面量
  • 不能与 Number 类型直接混合运算
  • 不能使用 Math 对象的方法
  • 只能表示整数,不支持小数

函数参数的增强

默认参数

function foo(x = 1, y = 1) {
    return x + y;
}
console.log(foo(3)); // 4 (使用默认值 y=1)
console.log(foo(3, 5)); // 8
console.log(foo()); // 2 (使用默认值 x=1, y=1)

剩余参数 vs arguments

function foo(...args) {
    console.log('剩余参数:', args, typeof args); 
    // [1, 2, 3, 4] 'object'
    console.log('是数组吗:', Array.isArray(args)); // true
    
    console.log('arguments:', arguments, typeof arguments); 
    // [Arguments] { '0': 1, '1': 2, '2': 3, '3': 4 } 'object'
    console.log('arguments是数组吗:', Array.isArray(arguments)); // false
    
    // 剩余参数支持数组方法
    console.log('参数个数:', args.length);
    console.log('参数总和:', args.reduce((a, b) => a + b, 0));
}

foo(1, 2, 3, 4);

剩余参数的优势:

  • 是真正的数组,可以使用所有数组方法
  • 更清晰的语法
  • 更好的类型推断(TypeScript)

其他实用特性

指数运算符

// ES7 (2016) 引入的指数运算符
console.log(2 ** 10); // 1024
console.log(3 ** 3); // 27

// 替代 Math.pow()
console.log(Math.pow(2, 10)); // 1024 (传统方式)

对象属性简写

const name = 'Alice';
const age = 25;

// 传统写法
const obj1 = {
    name: name,
    age: age
};

// ES6 简写写法
const obj2 = {
    name,
    age
};

console.log(obj2); // { name: 'Alice', age: 25 }

总结

ES6+ 的新特性让 JavaScript 开发变得更加优雅和高效:

  • 解构赋值 让数据提取更直观
  • 模板字符串 让字符串处理更简洁
  • for...of 提供更好的循环体验
  • BigInt 解决大整数计算问题
  • 函数参数增强 提供更灵活的函数设计

这些特性不仅提高了开发效率,也让代码更易读、易维护。建议在实际项目中积极采用这些现代 JavaScript 特性,提升代码质量。

学习建议:从解构赋值和模板字符串开始,逐步掌握其他特性,让 ES6+ 成为你的开发利器!

深入理解 JavaScript 异步编程:从 Ajax 到 Promise

在现代 Web 开发中,异步编程是不可或缺的核心概念。本文将通过几个实际的代码示例,带你深入理解 JavaScript 中的异步操作,从传统的 Ajax 到现代的 Promise 和 Fetch API。

1. 传统 Ajax 与现代 Fetch API

XMLHttpRequest:经典的异步请求方式

<script>
    const xhr = new XMLHttpRequest();
    xhr.open('GET', 'https://api.github.com/orgs/lemoncode/members', true);
    xhr.send();
    xhr.onreadystatechange = function() {
        if(xhr.readyState === 4 && xhr.status === 200) {
            const data = JSON.parse(xhr.responseText);
            console.log(data);
        }
    }
</script>

XMLHttpRequest 是传统的异步请求方式,基于回调函数实现。需要手动处理 readyState 和 status 状态码,代码相对复杂。

Fetch API:现代化的替代方案

<script>
    fetch('https://api.github.com/orgs/lemoncode/members')
        .then(res => res.json())
        .then(data => {
            console.log(data);
        })
</script>

Fetch API 的优势:

  • 基于 Promise 实现,支持链式调用
  • 语法简洁,无需手动处理状态码
  • 更符合现代 JavaScript 编程风格

2. 封装基于 Promise 的 getJSON 函数

为了将传统的 Ajax 改造成 Promise 风格,我们可以封装一个 getJSON 函数:

const getJSON = (url) => {
    return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open('GET', url, true);
        xhr.send();
        xhr.onreadystatechange = function() {
            if(xhr.readyState === 4) {
                if(xhr.status === 200) {
                    const data = JSON.parse(xhr.responseText);
                    resolve(data);
                } else {
                    reject(`HTTP错误: ${xhr.status}`);
                }
            }
        }
        xhr.onerror = function() {
            reject('网络错误');
        }
    });
}

// 使用示例
getJSON('https://api.github.com/orgs/lemoncode/members')
    .then(data => {
        console.log(data);
    })
    .catch(err => {
        console.log(err);
    })

关键点说明:

  • Promise 构造函数接收一个执行器函数,同步执行
  • resolve() 将 Promise 状态改为 fulfilled,触发 then 回调
  • reject() 将 Promise 状态改为 rejected,触发 catch 回调
  • onerror 只能捕获网络错误,不能捕获 HTTP 错误状态码

3. Promise 状态管理

状态 (State) 含义 说明
pending (等待中) 初始状态 Promise 被创建后,尚未被兑现或拒绝时的状态
fulfilled (已成功) 操作成功完成 异步任务成功结束,调用了 resolve(value)
rejected (已失败) 操作失败 异步任务出错,调用了 reject(reason)

4. 实现 Sleep 函数:控制异步流程

在同步语言中,我们可以使用 sleep 函数暂停程序执行,但在 JavaScript 中需要借助 Promise 模拟这一行为:

// 基础版本
function sleep(n) {
    let p;
    p = new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log(p); // pending 状态
            resolve(); // 或 reject()
            console.log(p); // fulfilled/rejected 状态
        }, n);
    });
    return p;
}

// 简化版本
const sleep = n => new Promise(resolve => setTimeout(resolve, n));

// 使用示例
sleep(3000)
    .then(() => {
        console.log('3秒后执行');
    })
    .catch(() => {
        console.log('error');
    })
    .finally(() => {
        console.log('finally'); // 无论成功失败都会执行
    });

setTimeout vs Sleep:本质区别

setTimeout - 事件调度

console.log("第1步");
setTimeout(() => {
    console.log("第3步 - 在2秒后执行");
}, 2000);
console.log("第2步 - 不会等待setTimeout");

// 输出顺序:
// 第1步
// 第2步 - 不会等待setTimeout
// 第3步 - 在2秒后执行

Sleep - 流程控制

async function demo() {
    console.log("第1步");
    await sleep(2000);  // 真正暂停函数的执行
    console.log("第2步 - 在2秒后执行");
}

demo();

// 输出顺序:
// 第1步
// (等待2秒)
// 第2步 - 在2秒后执行

这么来说吧:setTimeout是告诉 JavaScript 引擎:“等 X 毫秒后,帮我执行这个函数。” 它会去做别的同步操作,不会阻塞其他代码的执行。sleep则是在一个连续的异步操作流中,插入一段等待时间。sleep 让你写出“顺序等待”的逻辑,而 setTimeout 只是“未来某个时间点做某事”。

5. JavaScript 内存管理与数据拷贝

理解 JavaScript 的内存管理对于编写高效代码至关重要:

const arr = [1, 2, 3, 4, 5, 6];

const arr2 = [].concat(arr); 
arr2[0] = 0;
console.log(arr, arr2); // arr 不变,arr2 改变

// 深拷贝 - 开销大
const arr3 = JSON.parse(JSON.stringify(arr));
arr3[0] = 100;
console.log(arr, arr3); // arr 不变,arr3 改变

在数组上进行存储的时候我们应该尽量规避JSON序列化的深拷贝,开销太大,我们可以选择用[].cancat(arr)这种巧妙的方法实现拷贝,虽然创建了一个新数组,但开销仍然小于序列化。

内存管理要点

  1. JS 变量在编译阶段分配内存空间
  2. 简单数据类型存储在栈内存中
  3. 复杂数据类型存储在堆内存中,栈内存存储引用地址
  4. 浅拷贝只复制引用,深拷贝创建完全独立的新对象

总结

通过本文的示例和解析,我们深入探讨了:

  1. Ajax 与 Fetch 的对比:Fetch API 提供了更简洁的 Promise-based 接口
  2. 手写getJson函数:将传统的 Ajax 改造成 现代的 Promise 风格
  3. 手写sleep函数:使用 Promise 实现类似同步的编程体验
  4. 内存管理:理解变量存储方式对编写高效代码的重要性

掌握这些概念将帮助你编写更清晰、更易维护的异步 JavaScript 代码,为学习更高级的 async/await 语法打下坚实基础。

React 日历组件完全指南:从网格生成到农历转换

本文详细介绍如何从零实现一个功能完整的 React 日历组件,包括日历网格生成、农历显示和月份切换功能。

前言

在开发排班管理系统时,我们需要实现一个功能完整的日历组件。这个组件不仅要显示标准的月历网格,还要支持农历显示和流畅的月份切换。经过实践,我总结了一套完整的实现方案,适用于任何 React 项目。

一、日历网格生成

1.1 核心需求

一个标准的月历网格需要满足以下要求:

  • 显示当前月份的所有日期
  • 补齐上月末尾的日期(填充第一周)
  • 补齐下月开头的日期(填充最后一周)
  • 总是显示完整的 6 周(42 天)
  • 周日为每周的第一天

1.2 实现思路

我们使用 date-fns 库来处理日期计算,整个算法分为三个步骤:

// DateService.ts
getMonthCalendarGrid(date: Date): Date[] {
  // Step 1: 获取月份的起止日期
  const monthStart = startOfMonth(date);
  const monthEnd = endOfMonth(date);
  
  // Step 2: 扩展到完整的周
  const calendarStart = startOfWeek(monthStart, { weekStartsOn: 0 });
  const calendarEnd = endOfWeek(monthEnd, { weekStartsOn: 0 });
  
  // Step 3: 生成日期数组
  return eachDayOfInterval({
    start: calendarStart,
    end: calendarEnd
  });
}

关键点解析:

  1. 获取月份边界:使用 startOfMonthendOfMonth 获取当月的第一天和最后一天
  2. 扩展到完整周:使用 startOfWeekendOfWeek 确保日历从周日开始,到周六结束
  3. 生成连续日期:使用 eachDayOfInterval 生成两个日期之间的所有日期

1.3 实际案例

以 2024 年 11 月为例:

输入:new Date(2024, 10, 15)  // 2024-11-15

Step 1: 月份边界
  monthStart = 2024-11-01 (周五)
  monthEnd   = 2024-11-30 (周六)

Step 2: 扩展到周
  calendarStart = 2024-10-27 (周日)
  calendarEnd   = 2024-11-30 (周六)

Step 3: 生成日期
  共 35 天 (5周)

渲染结果:
日  一  二  三  四  五  六
27  28  29  30  31   1   2   ← 10月27-31 + 11月1-2
 3   4   5   6   7   8   9
10  11  12  13  14  15  16
17  18  19  20  21  22  23
24  25  26  27  28  29  30

1.4 为什么是 6 周?

大多数月份只需要 5 周(35 天)就能显示完整,但某些特殊情况需要 6 周(42 天)。

需要 6 周的条件:

  • 月份有 31 天
  • 月初是周六(需要补充前面 6 天)

为了保持布局一致性,我们统一使用 6 周布局,这样月份切换时高度不变,动画过渡更流畅。

二、农历(阴历)显示

2.1 实现原理

农历转换使用预定义数据表 + 算法计算的方式,无需外部依赖,支持 1900-2100 年。

2.2 数据结构

农历信息表

private static lunarInfo = [
  0x04bd8, 0x04ae0, 0x0a570, 0x054d5, 0x0d260,  // 1900-1904
  // ... 共 201 个元素(1900-2100年)
];

每个十六进制数编码了一年的农历信息:

0x04bd8 的二进制表示:
0000 0100 1011 1101 1000

解析:
├─ 后 4 位 (1000 = 8):闰月位置(8月)
├─ 第 5 位 (1):闰月天数(1=30天,0=29天)
└─ 第 6-17 位:每月天数(1=30天,0=29天)

农历日期文本

private static lunarDays = [
  '初一', '初二', '初三', ..., '廿九', '三十'
];

2.3 转换算法

公历转农历分为四个步骤:

Step 1: 计算与基准日期的天数差
  基准:1900-01-31(农历1900年正月初一)
  
Step 2: 从1900年开始,逐年累减天数,确定农历年份

Step 3: 逐月累减天数,确定农历月份(处理闰月)

Step 4: 剩余天数 + 1 = 农历日期

2.4 实际案例

以 2024-11-24 为例:

Step 1: 天数差
  (2024-11-24 - 1900-01-31) = 45590 

Step 2: 确定农历年
  1900年:354天,剩余 45236
  1901年:354天,剩余 44882
  ...
  2023年:384天,剩余 324
   农历2024年

Step 3: 确定农历月
  正月:30天,剩余 294
  二月:29天,剩余 265
  ...
  十月:30天,剩余 29
   农历十月

Step 4: 确定农历日
  29 + 1 = 30
   三十

结果:2024-11-24 = 农历2024年十月三十

2.5 使用方法

// 获取农历日期文本
const lunarText = LunarUtil.getLunarDateText(new Date(2024, 10, 24));
console.log(lunarText);  // 输出:三十

// 在日历中应用
<div className="day-cell">
  <div className="day-text">{date.getDate()}</div>
  <div className="lunar-text">
    {LunarUtil.getLunarDateText(date)}
  </div>
</div>

渲染效果:

┌─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ 24  │ 25  │ 26  │ 27  │ 28  │ 29  │ 30  │
│廿四 │廿五 │廿六 │廿七 │廿八 │廿九 │三十 │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┘

三、月份切换功能

3.1 核心思路

月份切换的本质是改变当前显示的月份,然后重新生成日历网格。

核心要素:

  1. 维护一个 currentDate 状态
  2. 提供切换方法(上一月/下一月)
  3. 根据 currentDate 重新生成日历网格

3.2 状态管理

const [currentDate, setCurrentDate] = useState(new Date());

currentDate 的作用:

  • 决定显示哪个月份
  • 作为生成日历网格的输入

3.3 切换方法

使用 date-fns 实现月份切换:

import { addMonths, subMonths } from 'date-fns';

// 下一个月
const goToNextMonth = () => {
  setCurrentDate(prevDate => addMonths(prevDate, 1));
};

// 上一个月
const goToPrevMonth = () => {
  setCurrentDate(prevDate => subMonths(prevDate, 1));
};

// 通用方法
const handleMonthChange = (direction: 'next' | 'prev') => {
  setCurrentDate(prevDate => {
    return direction === 'next' 
      ? addMonths(prevDate, 1)
      : subMonths(prevDate, 1);
  });
};

3.4 自动处理边界

JavaScript Date 构造函数会自动处理月份溢出:

// 12月 → 1月(跨年)
new Date(2024, 12, 1)  // 自动变为 2025-01-01

// 1月 → 12月(跨年)
new Date(2024, -1, 1)  // 自动变为 2023-12-01

3.5 响应式更新

使用 useMemo 实现响应式更新:

const MonthView: React.FC<MonthViewProps> = ({ currentDate }) => {
  const currentMonthDates = useMemo(() => {
    return DateService.getMonthCalendarGrid(currentDate);
  }, [currentDate]);  // 依赖 currentDate
  
  // currentDate 变化 → useMemo 重新计算 → 生成新的日历网格
};

3.6 完整数据流

用户点击"下一月"
  ↓
setCurrentDate(新月份)
  ↓
useMemo 重新计算
  ↓
生成新的日历网格
  ↓
渲染新月份

四、完整实现

4.1 日历组件

import React, { useState, useMemo } from 'react';
import { addMonths, subMonths, format } from 'date-fns';

function Calendar() {
  const [currentDate, setCurrentDate] = useState(new Date());
  
  // 切换月份
  const handleMonthChange = (direction: 'next' | 'prev') => {
    setCurrentDate(prev => {
      return direction === 'next' 
        ? addMonths(prev, 1)
        : subMonths(prev, 1);
    });
  };
  
  // 生成日历网格
  const dates = useMemo(() => {
    return generateCalendarGrid(currentDate);
  }, [currentDate]);
  
  return (
    <div className="calendar">
      {/* 标题 */}
      <h2>{format(currentDate, 'yyyy年MM月')}</h2>
      
      {/* 切换按钮 */}
      <button onClick={() => handleMonthChange('prev')}>上一月</button>
      <button onClick={() => handleMonthChange('next')}>下一月</button>
      
      {/* 日历网格 */}
      <CalendarGrid dates={dates} />
    </div>
  );
}

4.2 渲染网格

function CalendarGrid({ dates }) {
  // 分组为周
  const weeks = [];
  for (let i = 0; i < dates.length; i += 7) {
    weeks.push(dates.slice(i, i + 7));
  }
  
  return (
    <div className="calendar-grid">
      {/* 星期头部 */}
      <div className="week-header">
        {['日', '一', '二', '三', '四', '五', '六'].map(day => (
          <div key={day} className="week-day">{day}</div>
        ))}
      </div>
      
      {/* 日历网格 */}
      {weeks.map((week, weekIndex) => (
        <div key={weekIndex} className="week-row">
          {week.map((date, dayIndex) => (
            <div key={dayIndex} className="day-cell">
              {/* 公历日期 */}
              <div className="day-text">{date.getDate()}</div>
              
              {/* 农历日期 */}
              <div className="lunar-text">
                {LunarUtil.getLunarDateText(date)}
              </div>
            </div>
          ))}
        </div>
      ))}
    </div>
  );
}

五、性能优化

5.1 使用 useMemo 缓存计算

// 缓存日历网格
const dates = useMemo(() => {
  return generateCalendarGrid(currentDate);
}, [currentDate]);

5.2 使用 useCallback 缓存回调

const handleDatePress = useCallback((date: Date) => {
  onDatePress(date);
}, [onDatePress]);

5.3 使用 React.memo 避免无效渲染

export default React.memo(MonthView);

六、关键要点总结

6.1 日历网格生成

核心 API:

  • startOfMonth / endOfMonth - 获取月份边界
  • startOfWeek / endOfWeek - 扩展到完整周
  • eachDayOfInterval - 生成连续日期

数据结构:

Date[] (35-42个元素)
  ↓ 分组
Date[][] (5-6个数组,每个7个元素)
  ↓ 渲染
6行 × 7列的网格

6.2 农历转换

核心算法:

  1. 计算天数差 - 与基准日期(1900-01-31)的差值
  2. 确定农历年 - 逐年累减天数
  3. 确定农历月 - 逐月累减天数,处理闰月
  4. 确定农历日 - 剩余天数 + 1

数据结构:

  • 预定义表 - 201个十六进制数(1900-2100年)
  • 位运算 - 高效解析农历信息
  • 文本数组 - 30个农历日期名称

6.3 月份切换

核心流程:

状态变化 → 网格重新生成 → 数据重新加载 → 组件重新渲染

关键技术:

  • useState - 状态管理
  • useMemo - 缓存计算结果
  • useEffect - 监听变化,自动加载数据
  • JavaScript Date - 自动处理月份边界

七、总结

通过本文,我们实现了一个功能完整的 React 日历组件,包括:

✅ 标准的月历网格生成(支持 5-6 周布局) ✅ 农历显示(支持 1900-2100 年) ✅ 流畅的月份切换(自动处理跨年) ✅ 响应式数据更新(状态驱动) ✅ 性能优化(useMemo、useCallback、React.memo)

核心思想是利用 date-fns 处理日期计算,使用 React Hooks 实现响应式更新,通过预定义数据表实现农历转换。整个实现简洁高效,易于维护和扩展。

这套方案不仅适用于 Web 应用,也可以轻松移植到 React Native 等其他 React 生态项目中。

希望这篇文章能帮助你理解日历组件的实现原理,并应用到自己的项目中。


相关资源:

简单聊聊webpack摇树的原理

Webpack 的 Tree Shaking(摇树)是一项用于消除 JavaScript 上下文中未引用代码的优化手段,它能有效减小打包体积。

核心原理

Tree Shaking 的本质是 死代码消除,它依赖 ES6 模块(ESM)的静态语法结构

  1. 静态分析:ESM 的 import/export 语句必须位于模块顶层(注意:模块顶层不是模块文件顶部的意思,模块顶层可以认为是模块文件中最外层的代码区,不在任何函数、类或代码块内部),且模块路径必须是字符串常量。这样, Webpack 在编译阶段就能构建出完整的模块依赖图,无需运行代码即可分析出哪些导出值未被其他模块使用 。

    这时有同学就会问了,那么动态 import 怎么判断呢?

    其实,还是那个关键点,是否可以被“静态分析”。

    // ❌ 难以静态分析,无法使用摇树优化
    const componentMap = {
      basic: () => import('./BasicComponent'),
      advanced: () => import('./AdvancedComponent')
    };
    const getComponent = componentMap[userInput]; // 运行时才能确定
    
    // ✅ 条件明确,可以被静态分析
    if (import.meta.env.VITE_APP_MODE === 'basic') {
      const BasicComponent = await import('./BasicComponent');
    }
    
  2. 标记与清除:Webpack 的 Tree Shaking 过程大致分为两步。首先,在编译阶段,Webpack 会遍历所有模块,标记(Mark) 出未被使用的导出(通常会在注释中生成类似 unused harmony export 的提示)。随后,在代码压缩阶段,Terser 等压缩工具会真正将标记过的"死代码"清除(Shake) 掉 。

这些配置你是否清楚?

要让 Tree Shaking 生效,需要同时满足以下条件:

  1. 使用 ES6 模块语法:必须使用 importexport 语句。CommonJS 的 requiremodule.exports动态的,无法在编译时进行静态分析,因此不支持 Tree Shaking 。
  2. 启用生产模式或明确配置:在 Webpack 配置中,将 mode 设置为 'production' 生产模式下会自动开启相关的优化功能。当然也可以在开发模式下手动配置 optimization.usedExportsoptimization.minimize
// webpack.config.js
module.exports = {
  mode: 'production', // 生产模式自动开启优化
  optimization: {
    usedExports: true, // 启用使用导出分析
    minimize: true     // 启用代码压缩(清除死代码)
  }
};
  1. **正确声明副作用 (sideEffects)**:在项目的 package.json 中,通过 sideEffects 属性告知 Webpack 哪些文件是"纯净"的(无副作用),可以安全移除。这能防止具有副作用的文件(如全局样式表、polyfill)被误删 。
// package.json
{
  "sideEffects": false, // 表示整个项目都没有副作用
  // 或明确指定有副作用的文件
  "sideEffects": [
    "**/*.css",
    "./src/polyfill.js"
  ]
}

有同学又会问了,摇树摇的不是 js 吗,样式表 css 怎么会被摇掉呢?

其实,这里指的是导入的但是没有明确导出的 css 样式表,导入导出是明确的 js 语句,css 是“副作用”,比如:

  • 仅导入但未使用任何导出(如 import './style.css'),属于是无形的“使用”,可能被误删
  • 使用 CSS Modules(如 import styles from './Component.module.css'),被视为有被使用的对象(如 styles.className),通常不会被误删

这些问题你遇到过吗?

开发过程中,以下情况仍可能导致 Tree Shaking 失效,看看你有没有遇到过:

  • Babel 配置不当:Babel 预设 @babel/preset-env 可能会将 ESM 转换为 CommonJS。务必确保其 modules 选项设置为 false,只有 ESM 可以摇树。
// .babelrc
{
  "presets": [["@babel/preset-env", { "modules": false }]]
}
  • 第三方库的模块版本:优先选择提供 ES6 模块版本的库(如使用 lodash-es 而非 lodash),并采用按需导入的方式 。
// 推荐:按需导入
import { debounce } from 'lodash-es';
// 不推荐:整体导入
import _ from 'lodash';
  • 导出粒度太粗:尽量使用具名导出而非默认导出对象,有助于进行更精细的分析 。
// 推荐:细粒度导出
export function func1() {}
export function func2() {}

// 谨慎使用:粗粒度导出(不利于分析内部未使用属性)
export default { func1, func2 };

使用 svgfmt 优化 SVG 图标

前言:SVG 图标的常见问题

在日常开发中,我们经常需要使用设计师提供的 SVG 图标。然而,从 Figma 等设计工具导出的 SVG 文件往往存在一些问题:

首先是代码冗余。设计工具导出的 SVG 通常包含大量不必要的标签,如 <defs><g><clipPath> 等容器元素,这些元素在实际使用中往往是多余的。其次,颜色值被写死。SVG 中的颜色通常以 fill="#333333" 这样的形式硬编码,导致无法像 icon font 那样通过 CSS 的 color 属性动态控制图标颜色。此外,文件中还可能包含不必要的属性和元数据,使得文件体积偏大。

这些问题给开发者带来了不少困扰:无法灵活控制图标样式,手动清理每个文件效率低下且容易出错,难以构建统一的图标管理系统。

svgfmt 简介

svgfmt 是一个专门用于优化 SVG 图标的开源工具,它采用 monorepo 架构,包含两个核心包:

  • @svgfmt/cli:命令行工具,提供便捷的文件处理能力,支持单文件和批量处理
  • @svgfmt/core:核心库,可以轻松集成到自定义脚本和构建流程中

svgfmt 能够自动清理冗余标签、移除固定颜色值、合并路径,让 SVG 图标变得更加轻量和易于维护。

使用指南

命令行使用(@svgfmt/cli)

首先安装 CLI 工具:

npm install -g @svgfmt/cli

基础用法

# 格式化单个文件(原地修改)
svgfmt icon.svg

# 批量处理并输出到指定目录
svgfmt "icons/**/*.svg" -o dist/icons

# 处理单个文件并输出到新位置
svgfmt logo.svg -o logo-optimized.svg

自定义转换

svgfmt 支持通过 --transform 参数自定义转换逻辑:

# 使用转换文件
svgfmt icons/*.svg --transform ./transform.js

# 使用内联代码
svgfmt icons/*.svg -t 'svg => svg.replace(/<svg/, "<svg class=\"icon\"")'

转换文件示例(transform.js):

export default function(svg) {
  return svg.replace(/<svg/, '<svg class="icon"');
}

// 或者使用命名导出
export function transform(svg) {
  return svg.replace(/<svg/, '<svg data-processed="true"');
}

编程方式使用(@svgfmt/core)

在项目中安装核心库:

npm install @svgfmt/core

基础示例

import { format } from '@svgfmt/core';

const svgContent = `<svg>...</svg>`;
const optimizedSvg = await format(svgContent);

高级配置

import { format } from '@svgfmt/core';

const result = await format(svgContent, {
  // 提高路径追踪精度(默认 600)
  traceResolution: 800,
  
  // 自定义转换函数
  transform: (svg) => {
    return svg.replace(/<svg/, '<svg class="custom-icon"');
  }
});

对于需要异步处理的场景,transform 函数也支持异步:

const result = await format(svgContent, {
  transform: async (svg) => {
    // 执行异步操作
    const processed = await someAsyncOperation(svg);
    return processed;
  }
});

优化效果对比

让我们看一个实际的优化案例:

优化前

<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
  <g opacity="0.6" clip-path="url(#clip0_144244_67656)">
    <circle cx="6" cy="6" r="5" stroke="#333" style="stroke:#333;stroke-opacity:1;" />
    <path d="M6 3.5V6L7.25 7.25" stroke="#333" style="stroke:#333;stroke-opacity:1;" />
  </g>
  <defs>
    <clipPath id="clip0_144244_67656">
      <rect width="12" height="12" fill="#333" style="fill:#333;fill-opacity:1;" />
    </clipPath>
  </defs>
</svg>

优化后

<svg viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
  <path
    d="M5.660 0.521 C 4.899 0.566,4.190 0.760,3.500 1.111 C 2.479 1.630,1.630 2.479,1.111 3.500 C 0.702 4.303,0.516 5.086,0.516 6.000 C 0.516 6.748,0.635 7.374,0.904 8.050 C 1.386 9.261,2.332 10.295,3.500 10.889 C 4.085 11.187,4.680 11.369,5.332 11.451 C 5.675 11.494,6.325 11.494,6.668 11.451 C 8.173 11.262,9.516 10.479,10.410 9.270 C 10.792 8.753,11.059 8.227,11.248 7.620 C 11.517 6.754,11.562 5.741,11.369 4.842 C 11.027 3.241,9.959 1.853,8.500 1.111 C 7.617 0.662,6.653 0.461,5.660 0.521 M6.720 1.549 C 7.447 1.673,8.126 1.965,8.720 2.408 C 8.964 2.590,9.410 3.036,9.592 3.280 C 10.040 3.880,10.330 4.559,10.454 5.298 C 10.505 5.599,10.505 6.401,10.454 6.702 C 10.330 7.441,10.040 8.120,9.592 8.720 C 9.410 8.964,8.964 9.410,8.720 9.592 C 8.120 10.040,7.441 10.330,6.702 10.454 C 6.401 10.505,5.599 10.505,5.298 10.454 C 4.559 10.330,3.880 10.040,3.280 9.592 C 3.035 9.410,2.589 8.963,2.408 8.720 C 1.955 8.111,1.671 7.445,1.546 6.702 C 1.495 6.401,1.495 5.599,1.546 5.298 C 1.671 4.556,1.955 3.891,2.408 3.280 C 2.588 3.036,3.036 2.588,3.280 2.408 C 3.968 1.898,4.680 1.618,5.560 1.512 C 5.729 1.492,6.537 1.517,6.720 1.549 M5.500 4.845 L 5.500 6.190 6.200 6.890 L 6.900 7.590 7.245 7.245 L 7.590 6.900 7.045 6.355 L 6.500 5.810 6.500 4.655 L 6.500 3.500 6.000 3.500 L 5.500 3.500 5.500 4.845"
    fill-rule="evenodd"
  />
</svg>

主要改进包括:

  • 移除冗余标签:去除了不必要的 <defs><g><clipPath> 等容器元素
  • 颜色属性移除:删除固定的 fill 属性,图标将继承父元素的 color,可以通过 CSS 灵活控制
  • 路径合并:多个子元素被智能合并为单一路径

实践场景

集成到开发流程

package.json 中添加脚本命令:

{
  "scripts": {
    "optimize-icons": "svgfmt raw-icons/**/*.svg -o src/assets/icons",
    "build": "npm run optimize-icons && vite build"
  }
}

这样在每次构建前都会自动优化图标文件。

配合构建工具使用

在构建脚本中使用编程 API:

import { formatPattern } from '@svgfmt/cli';

// 在构建时自动优化图标
async function buildIcons() {
  const summary = await formatPattern('icons/**/*.svg', {
    output: 'dist/icons'
  });

  console.log(`优化完成: ${summary.success}/${summary.total}`);
  
  // 检查单个文件结果
  for (const result of summary.results) {
    if (result.success) {
      console.log(`✓ ${result.input}${result.output}`);
    } else {
      console.error(`✗ ${result.input}: ${result.error}`);
    }
  }
}

buildIcons();

自定义转换示例

创建一个转换文件 transform.js,为所有 SVG 添加统一属性:

export default function(svg) {
  return svg
    .replace(/<svg/, '<svg class="icon"')
    .replace(/viewBox/, 'preserveAspectRatio="xMidYMid meet" viewBox');
}

然后在命令行中使用:

svgfmt icons/*.svg --transform ./transform.js -o dist/icons

注意事项

在使用 svgfmt 时,有几点需要注意:

  1. 仅支持单色图标:该工具专为单色图标设计。多色 SVG 在路径追踪过程中会被转换为单色,因为工具会将 SVG 转换为 PNG 再转回 SVG,这个过程会丢失颜色信息。

  2. 路径精度配置traceResolution 参数控制路径追踪的精度,默认值为 600。提高该值可以获得更精细的路径,但会增加处理时间。建议的取值范围是 600-1200。

  3. 文件覆盖提醒:使用命令行工具时,默认会覆盖原文件。建议在处理前先备份原始文件,或使用 -o 参数指定输出目录。

  4. 复杂图形检查:对于复杂的多色插图或渐变效果的 SVG,建议在优化后手动检查结果,确保视觉效果符合预期。

  5. 工具组合:svgfmt 可以与其他 SVG 工具配合使用,如 SVGO(进一步优化)、svgr(转换为 React 组件)等,构建完整的 SVG 处理流程。

总结

svgfmt 通过自动化的方式解决了 SVG 图标在实际使用中的常见问题,让图标文件更轻量、更易维护。无论是通过命令行快速处理一批图标,还是将其集成到构建流程中实现自动化优化,svgfmt 都能显著提升开发效率。

项目已在 GitHub 开源,包含完整的文档和示例。如果你在项目中遇到 SVG 图标管理的问题,不妨试试 svgfmt,欢迎使用和贡献代码。

鸣谢

博客文章

如何将一个 React SPA 项目迁移到 Next.js 服务端渲染

日期:2025年11月24日

价值

  1. “渐进式迁移”策略:学会最大程度保护现有投资,实现技术栈的平稳升级帮助学员使用 Next.js 实现服务端渲染 。
  2. 征服服务端渲染,解决核心业务痛点: 您将系统掌握 Next.js 的核心能力(SSR/SSG/ISR),从根本上解决传统 React SPA 面临的页面加载缓慢、SEO 不友好两大难题,从而提升用户体验和商业转化率。

前言

  1. 本文将聚焦渲染原理,不深入讲解 Next.js 的具体使用细节,而是通过一个真实案例,以战代练的方式带你将 React SPA 应用迁移至 Next.js 框架。
  2. 结合大量实战项目中的踩坑经验,降低上手门槛,并借助第三方生态,助你构建完整的 Next.js 基础能力体系。
  3. 提供一套符合落地要求的 Next.js 构建与部署实用教程。

目标

  1. 对服务端渲染祛魅,深入理解与掌握 SSR、SSG、CSR 等渲染模式的原理、优缺点及适用场景。
  2. 具备平滑迁移与架构设计手段,具备技术落地能力。
  3. 能够将 Next.js 应用顺利部署到云上环境。

目录/结构

  1. 第一个模块主要包括如何最简单的上手 Next.js,包括环境准备、基本的渲染原理、与 SPA 应用的开发差异
  2. 第二个模块用一个实际大型网站迁移路线,说明我们从一个大型网站如何一步一步重构成Next应用,从而实现网页性能优化的过程。
  3. 第三个模块包含了如何进行落地的部署方案

第一章:Next.js 最简单的上手教程

如何最低成本的上手 Next.js 呢?

内心OS:

  • 之前也没有学过相关的知识。是不是很难啊?如果很难的话那算了,我还是回去画我的页面吧/_ \

第一节:前置准备

1、开发环境

基于Next 15 最新api,所以对运行环境有一定要求

  • Node.js 18.18 或更高LTS版本
  • macOS、Windows(含 WSL)或 Linux 系统

2、项目初始化

为了大家更好的上手,我准备了两套可用于生产环境的模版供大家使用

  • 通用版本:codelab.msxf.com/public-repo…

  • 精简版本:codelab.msxf.com/public-repo…

    • 能力 通用版本 精简版本
      React 版本 19.2.0 18.3.0
      组件库 Ant Design v5 - -
      应用状态管理 Zustand - -
      服务端请求 demo - -
      客户端请求 demo - -
      CSS预处理器:Sass - -
      代码检查 Typescript,Eslint 等 - -
      单元测试 Jest - -
      多环境部署方案 - -
      自定义服务器 Koa - -
      css 样式烘焙 - -
      前端监控 @sentry/react - -
      前端埋点 UBS - -
      微前端方案 qiankun 2.10.16,应用动态更新,微前端通信 模块联邦 2.0

第二节:认识文件系统约定

1、认识组件层级结构

  • layout.js布局文件,默认Server Components
  • template.js 同 layout.js,但是在导航时重新同步执行 useEffect
  • error.js (React 错误边界)
  • loading.js (React Suspense 边界)
  • not-found.js (notFound 函数执行时渲染 UI)
  • page.js 或嵌套的 layout.js(必要的)

2、了解路由嵌套关系

嵌套文件夹定义了路由结构。每个文件夹代表一个路由段,对应 URL 路径中的一个段。只有当路由段中添加了 page.jsroute.js 文件时,该路由才会对外可访问

  • 嵌套路由

    • 最基础的文件路由关系

    • 规则 说明 实例 用户URL
      folder 路由段 app/folder/page.js /folder
      folder/folder 嵌套路由段 app/folder/folder/page.js /folder/folder
  • 动态路由

    • 无法提前确定确切的路由段名称,并希望根据动态数据创建路由时

    • 规则 说明 实例 用户URL
      [folder] 动态路由段 app/[slug]/page.js /``shop``/``shop1
      [...folder] 全捕获路由段 app/shop/[...slug]/page.js /shop/a``/shop/a/b``/shop/a/b/c
      [[...folder]] 可选全捕获路由段 通配段和可选通配段的区别在于,可选情况下,不带参数的路由也会被匹配(如上例中的 /shop 以上
    •   例如,博客可以包含以下路由 app/blog/[slug]/page.js,其中 [slug] 是博客文章的动态段
    •   export default async function Page({
          params,
        }: {
          params: Promise<{ slug: string }>
        }) {
          const { slug } = await params
          return <div>我的文章: {slug}</div>
        }
      
  • 路由组与私有文件夹

    • 表示该文件夹仅用于组织目的,通常用来引入公共布局

    • 规则 说明 实例 用户URL
      (folder) 不影响实际路由的分组 app/(shop)/a/page.js /a
      _folder 将文件夹及其子路由段排除在路由系统外 - -

  • 拦截路由

    • 在后退导航时关闭模态框而非返回上一路由
    • 在前进导航时重新打开模态框
    • 规则 说明 实例
      (.)folder 拦截同级路由 -
      (..)folder 拦截上一级路由 -
      (..)(..)folder 拦截上两级路由 -
      (...)folder 从根路由拦截 -
    •   例如,当点击信息流中的照片时,你可以在模态框中显示该照片并覆盖在信息流上方。这种情况下,Next.js 会拦截 /photo/123 路由,隐藏 URL 并将其覆盖在 /feed 之上。
  • 并行路由

    • 规则 说明 实例
      @folder 命名插槽,一个路由命中多个页面 见下方说明

在一个仪表盘应用中,您可以使用并行路由同时或条件性渲染 teamanalytics 两个页面

第三节:页面开发

1、页面概念-RSC渲染机制

Next.js 15 的 App Router 通过服务端组件RSC渲染机制(服务端组件/客户端组件)模糊了传统 SSR 和 CSR 的严格界限,通过混合渲染模式开发者只需关注“服务端逻辑”与“客户端逻辑”的划分,而非显式选择渲染模式

第一个需要改变的思维模式是从“页面级”的渲染转变为“组件级”的渲染

假如我们有一个组件如下:

// ServerComponent.js (一个 RSC)
import ClientComponent from './ClientComponent';

async function ServerComponent() {
  // 在服务器上直接进行数据获取
  const data = await fetch('/api/user');
  return (
    <div>
      {/* 服务器渲染这部分 */}
      <h1>我的博客</h1>
      {/* 这里“嵌入”了一个客户端组件 */}
      <ClientComponent initialPosts={data} />
    </div>
  );
}

在这个过程中发生了什么?

  1. 服务器执行 ServerComponent

  2. 服务器从接口/数据库获取 data

  3. 服务器渲染 <h1>我的博客</h1>

  4. 当服务器遇到 <ClientComponent>时,它会为这个客户端组件“留出一个位置”,并将 initialPosts作为 prop 序列化。

  5. 最终,服务器发出的流式传输响应包含:

    1. ServerComponent渲染出的 HTML 结构。
    2. 客户端组件位置的标记。
    3. 客户端组件所需的初始数据(序列化的 props)。
    4. 指向客户端组件所需 JavaScript 的链接。
  6. 浏览器收到响应后,会立即显示由服务端渲染好的 HTML 内容(极快的首屏显示)。

  7. 然后,React 会进行 Hydration。但这里的 Hydration 是细粒度的。它只会下载并激活客户端组件部分的 JavaScript,使它们变得可交互。服务端组件的 JavaScript 永远不会发送到客户端。

时间 服务器行为 用户看到什么
0s 开始渲染 空白屏
0.1s 遇到异步操作,立即发送 loading UI 看到静态内容部分看到 ServerComponent"加载中,请稍候..."+ 客户端下载资源、解析资源、渲染内容
2s 数据获取完成,发送实际内容 看到完整的页面内容

思考:当接口需要2s,那是不是所有用户都需要等待 2s loading 之后才能看到页面呢?

2、服务端渲染组件

适用场景:服务端获取数据渲染部分 UI,并将其流式传输到客户端。这些场景更适合服务端渲染

  • 提升首次内容绘制 (FCP),并逐步将内容流式传输到客户端
  • 从数据库或靠近数据源的 API 获取数据
  • 使用 API 密钥、令牌等敏感信息而不暴露给客户端
  • 减少发送到浏览器的 JavaScript 体积

默认情况下,组件都是服务端组件

// 引入客户端组件
import LikeButton from '@/app/ui/like-button'
import { getPost } from '@/lib/data'
import { db, posts } from '@/lib/db'

export default async function Page({ params }: { params: { id: string } }) {
  const post = await getPost(params.id)
  const allPosts = await db.select().from(posts)

  return (
    <div>
      <main>
        <h1>{post.title}</h1>
        {/* ... */}
        <LikeButton likes={post.likes} />
        <ul>      
          {allPosts.map((post) => (        
            <li key={post.id}>{post.title}</li>
          ))}    
        </ul>
      </main>
    </div>
  )
}

3、客户端渲染组件

适用场景:当需要交互性或使用浏览器 API

通过在文件顶部(导入语句之前)添加 "use client" 指令来创建客户端组件。

这时候将你的SPA页面复制到客户端组件中,也能完美运行。

'use client'

import { useState } from 'react'

export default function Counter() {
  const [count, setCount] = useState(0)

  return (
    <div>
      <p>{count} likes</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  )
}

注意: 在客户端组件中嵌套服务端渲染的 UI,需要服务端组件作为 prop 传递给客户端组件。不能在客户端组件中直接 import 引入服务端组件

'use client'

export default function Modal({ children }: { children: React.ReactNode }) {
  return <div>{children}</div>
}
import Modal from './ui/modal' // 客户端组件
import Cart from './ui/cart'   // 服务端组件

// 服务端组件
export default function Page() {
  return (
    <Modal>
      <Cart />
    </Modal>
  )
}

思考:为什么客户端组件不能直接 import 服务端组件,反过来服务端组件就可以呢??

第二章:从 CSR 到混合渲染迁移路线

第一节:生态对比关系图

云微前端主项目 Umi 4 到 Next 15

暂时无法在唯科之家2.0文档外展示此内容

第二节:生态迁移细节

1、webpack 迁移到 Turbopack

遇到的问题

  • Q1:没有了svgr,如何支持.svg 文件并将其渲染为 React 组件呢?

    •   // 使用 @svgr/webpack 加载器,该加载器支持导入 .svg 文件并将其渲染为 React 组件
      
        module.exports = {
          turbopack: {
            rules: {
              '*.svg': {
                loaders: ['@svgr/webpack'],
                as: '*.js',
              },
            },
          },
        }
      
  • Q2:umi 自定义插件迁移问题,通过插件启用Brotli静态压缩

    •   next方案不是很完美,默认情况下,当使用 next start 或自定义服务器时,Next.js 会使用 gzip 压缩渲染内容和静态文件。如果想要用Brotli压缩代码,则需要这样做:
    •   module.exports = {  
          compress: false,
        }
      
    •   如果您正在使用 nginx 并希望改用 brotli 压缩,可以将 compress 选项设为 false 以让 nginx 处理压缩(其实也并不完美,这种方案需要依赖服务器压缩,而不是构建时压缩)。

2、antd 升级

遇到的问题

  • Q1:为什么使用在 Next.js 中直接 <Select.Option /> 、<Typography.Text />会报错,需要从路径引入这些子组件来避免错误

    •   <Select.Option value="1">Option 1</Select.Option>
      
    •   A:服务器组件树序列化问题,当使用 Select.Option这样的点表示法时,实际上是在引用一个对象的属性。Next.js 的 RSC 系统需要能够静态分析组件树,以便正确序列化和在客户端重建组件树。
    •   说人话就是RSC 系统在编译时需要确定:哪些是服务端组件,哪些是客户端组件以及组件之间的边界关系
    •   见 ant.design/docs/react/…
  • Q2:服务端渲染中如何注入样式

    •   A:有两种方式在服务端渲染消费组件样式,各有好处

    • 内联:直接将样式内联到所使用的组件上

      • 好处是没有css请求
      • 缺点是如果使用了多个同样的组件,会内联多份相同的样式,造成 HTML 体积增大,影响首屏渲染速度
      •     见:ant.design/docs/react/…
    • 烘焙:类似 antd 4,将项目中使用过的组件样式提取出一份单独的css

      • 好处是打开任意页面时如传统 css 方案一样都会复用同一套 css 文件以命中缓存
      • 缺点是多主题的情况下会烘焙多份样式
      •   import React from 'react';
          import fs from 'fs';
          import { extractStyle } from '@ant-design/static-style-extract';
          import AntdConfigProvider from './AntdConfigProvider';
          import { IS_PRODUCTION } from '../src/constants/config';
        
          const outputPath = IS_PRODUCTION ? './public/antd.min.css' : './public/antd.environment.css';
        
          const css = extractStyle((node) => <AntdConfigProvider>{node}</AntdConfigProvider>);
        
          fs.writeFileSync(outputPath, css);
        

2、状态管理

从 umi 到 Zustand, 由于 Next.js 本身并不提供状态管理工具。我们需要自己选型一款稳定、简单、好用的状态管理工具

看看 React 各种 状态管理工具 Npm 下载趋势

Zustand 的使用本身很简单,定义一个Hooks,然后抛出状态和改变状态的方法即可

 // src/stores/counter-store.ts
import { create } from 'zustand/vanilla'

const useCountStore = create((set) => ({
    count: 0,
    inc: () => set((state) => ({ count: state.count + 1 })),
}))

// src/components/pages/home-page.tsx
import { useCountStore } from '@/providers/counter-store-provider.ts'

export const HomePage = () => {
  const { count, inc} = useCountStore ((state) => state)
}

但这是在SPA中使用,在服务端渲染中对 Zustand 使用不可变数据提出了一些独特的挑战,所以我们需要一点小小的改变

  • 定义一个 Provider 防止组件重复执行初始化数据

    •   import { createStore } from 'zustand';
        import { type ReactNode, createContext, useContext, useEffect, useRef } from 'react';
      
        export const createUserStore = () => {
          return createStore<UserStore>()((set) => ({
            userInfo: {},
            refreshUserFn: async (userInfo?: UserState ) => {
              set(() => ({ ...state, userInfo: userInfo }));
            },
          }));
        };
      
        export const UserStoreContext = createContext<UserStoreApi | undefined>(undefined);
      
        /**
        * 创建地域上下文
        * 避免 createUserStore 重复执行
        */
        export const UserStoreProvider = ({ children }: UserStoreProviderProps) => {
          const userStore = createUserStore();
          const storeRef = useRef<UserStoreApi>(null);
          if (!storeRef.current) {
            storeRef.current = userStore;
          }
          return <UserStoreContext.Provider value={userStore}>{children}</UserStoreContext.Provider>;
        };
      
        // 消费或初始化用户数据
        export const useUserStore = <T,>(selector: (store: UserStore) => T): T => {
          const UserContext = useContext(UserStoreContext);
          if (!UserContext) {
            throw new Error(`useUserStore must be used within UserStoreProvider`);
          }
          return useStore(UserContext, selector);
        };
      
  • 在全局入口挂载并初始化 Provider

    •   import { UserStoreProvider } from '@/providers/userStoreProvider';
        export default function Layout({children}) {
          return (
            <UserStoreProvider> {children}</UserStoreProvider >
          )
        }
      
  • 消费/变更用户数据

    •    const userInfo = useUserStore((state) => state.userInfo);
      

3、请求库

  • 服务端 fetch

    • Next.js 扩展了 Web fetch() API,允许服务器上的每个请求设置自己的持久化缓存和重新验证语义。

    •   新增两个api,配合 RSC 渲染实现缓存机制

    • cache :配置请求如何与 Next.js 数据缓存 (Data Cache) 交互

      • auto no cache (默认值):Next.js 会在每次请求时获取资源,且会在next build 时会获取一次
      • no-store: Next.js 会在每次请求时获取资源
      • force-cache: Next.js 会在其数据缓存中查找匹配的请求。如果找到匹配且未过期,将从缓存返回。如果没有匹配或匹配已过期,Next.js 将从远程服务器获取资源并更新缓存。
    • revalidate: 设置资源的缓存生命周期(以秒为单位)。

      • false - 无限期缓存资源。语义上等同于 revalidate: Infinity。HTTP 缓存可能会随时间推移淘汰旧资源。
      • 0 - 阻止资源被缓存。
      • number - (以秒为单位)指定资源的缓存生命周期最多为 n 秒。
  • 客户端 fetch

    • 同 window.fetch

4、css 预处理器

从 less 到 Sass

5、声明路由到文件路由

  • 特点总结:声明式路由和文件路由各有特点,声明路由上手简单,路由结构清晰;文件路由有一定上手门槛,控制颗粒度更细

    • 声明路由 文件路由 差异化
      重定向 redirct 文件拦截路由 文件拦截路由支持动态拦截,功能更强大
      声明嵌套路由 文件嵌套路由 无差异
      动态路由 文件动态路由 功能无差异。文件路由可以按需添加Loading.js,not-found.js等处理文件,控制颗粒度会更细,声明路由需要根据pathname手动处理公共逻辑
      - 文件并行路由 在设计条件路由、权限路由、标签页、URL模态框的时候比较有用

6、微前端

在 Next.js中实现 @umi/plugin-qiankun 插件功能

  • 原先umi框架中自带了一款非常好用的插件 @umi/plugin-qiankun 这让微前端接入、更新、状态传输变的异常简单,但是在 Next.js 中我们需要自己实现这个插件及相关功能

    • 暂时无法在唯科之家2.0文档外展示此内容

通过服务端server实现qiankun运行时注册,通过 Zustand + qiankun 实现子应用动态更新

第三章:部署方案

第一节:构建与部署

1、基础镜像

  • base/nodejs_nginx_anolios_brotli:v22.14.0_1.21.4.1

此前的基础镜像主要面向SPA应用,承担网络代理与静态资源转发的功能。在引入Next.js服务端渲染方案后,我们还需要部署一套Node服务。按照原有部署流程,需单独申请一个应用来运行Node服务,并额外配置一个Nginx应用,用于处理代理与静态资源相关配置。

为简化部署流程,我们现已重新构建了一款Base镜像,支持在单一实例中同时启动两个进程:Nginx服务负责请求转发和静态资源处理,而Node进程则用于运行Next.js服务。

2、构建

  • standalone

以前使用 Docker 部署时,需要安装包中 dependencies 的所有文件才能运行 next start。从 Next.js 12 开始,可以利用 .next/ 目录中的输出文件追踪功能.nft.json,仅包含必要的文件.next 目录

我们通过开启 standlone 特性,此时 Next.js 可以根据.nft.json自动创建一个 standalone 文件夹,仅复制生产部署所需的文件,包括 node_modules 中的选定文件。还会输出一个最小化的 server.js 文件,可用于替代 next start

优化之前构建时长+镜像制作(包含打包modules时间)共计15分钟。优化之后构建时长和镜像制作约3分钟,当然启用 standalone 也为我们带来一些挑战

  • 默认 next build 部署方式

  • 使用 standalone 构建

  • Q1 :当我们使用 pnpm 构建时,报错 Error: EPERM: operation not permitted, symlink

    •   A:windows系统非管理员权限问题 (相关Discussions),
    •    方法一:可以通过设置,这会扁平化pnpm依赖,需要注意的是幽灵依赖问题
    •    ## 禁用符号链接来避免此问题,避免非管理员 standalone 构建报错
        symlink=false
        node-linker=hoisted
      
    •    方法二:本地构建通过环境变量用 output: 'export', // 而不是 'standalone', 生产环境还是使用standalone,需要注意两种构建方式产物差异的问题,这可能会导致本地开发效果跟生产不一致
    •    方法三:换电脑 or 申请管理员权限
  • Q2:standalone 为了极致的性能甚至不会复制 public.next/static 文件夹,他默认我们会将这些文件内容部署到cdn

    •   A:当我们没有cdn服务器的时候,需要额外的复制这些文件到 .next文件夹下
    •   cp -r public .next/standalone/ && cp -r .next/static .next/standalone/.next/
      
  • Q3:如何自定义端口或主机名

    •   A:在启动命令中添加
    •   PORT=3000 HOSTNAME=0.0.0.0
      

3、部署

  • 关于启动命令,先启动 Next.js 相关 Node 服务器(内部服务器,包含环境变量、端口、主机名称),在启动Nginx 服务(提供外部访问,含动态域名解析)

    •   HOSTNAME=localhost SERVER_API_ENV=online node ./standalone/server.js & bash /opt/nginx/conf/resolve.sh && /opt/nginx/runnginx.sh
      

第二节:Nginx 配置说明

1、服务端代理

################ 网关配置 #####################
################ 网关配置 #####################
# 网关代理
location ~ ^/(noauth|portal)/ {
  set $proxy_url http://gc-gw-server.mscloud.lo;
  proxy_pass $proxy_url;
}

2、微前端子应用资源代理

通过子应用资源代理,子应用就不用申请公网域名

################ 主项目加载子项目静态页面配置 #####################
################ 主项目加载子项目静态页面配置 #####################
# 子应用转发
location ~ /msportal/(\w+)/(.*) {
  # 静态资源根据hash名称缓存
  add_header Cache-Control $cache_control_header;
  proxy_pass http://gc-$1-fe.mscloud.lo/$2$is_args$args;
}

3、Next.js 资源兜底

在配置 SPA Nginx 服务器的时候,通常我们会返回一个兜底的静态页面,防止应用白屏化。同样的,我们也可以在Nginx里面将没有代理到的请求通通转向 Next.js 应用,由应用来处理各种异常情况

################ 其他所有请求交给 Next.js 处理 #####################
################ 其他所有请求交给 Next.js 处理 #####################
# 核心Next.js资源
location /_next/static {
  proxy_pass http://localhost:3000;
  expires 30d;
}
# 静态资源缓存设置
location /public {
  proxy_pass http://localhost:3000;
  expires 30d;
}
# 核心路由代理(指向 Node 应用)
location / {
  add_header Cache-Control $cache_control_header;
  proxy_pass http://localhost:3000;
}

告别“屎山”:用 Husky + Prettier + ESLint 打造前端项目的代码基石

一个多人参与的项目,如果没有代码规范,会导致代码样式五花八门,特别难看。 我相信对于程序员这个职业来说,一个项目中,代码格式千奇百怪,没有几个人能忍吧。
现在在github上随意看一个项目,都几乎是配备了husky的,这个工具可以hook到git的命令,然后自动运行相关脚本,进而达到自动格式化代码的目的。

今天,我们就来详细介绍如何配置 Husky,并结合 Prettier 和 ESLint,为你的项目构建一个坚实的代码规范防线。

核心工具栈一览

在深入配置之前,我们先来认识一下我们将要使用的“黄金组合”:

  • Husky: Git Hooks 管理工具,负责拦截 Git 命令并在特定阶段执行脚本。
  • Prettier: 强大的代码格式化工具,确保代码风格统一美观。
  • ESLint: 静态代码分析工具,用于发现并修复代码中的潜在问题和不符合规范的写法。
  • lint-staged: 配合 Husky 使用,只对 Git 暂存区的文件进行 Lint 和格式化,大幅提升效率。
  • eslint-config-prettier: 解决 ESLint 与 Prettier 规则冲突的关键配置。

配置 Husky:自动化检查的“守门员”

husky官网,进去之后按照引导安装即可

# 安装 Husky
npm install --save-dev husky

# 初始化 Husky (会在项目根目录创建 .husky 文件夹)
npx husky init

小贴士: 如果你的项目尚未进行 Git 初始化 (git init),npx husky init 命令会报错提示 .git can't be found。请务必先初始化 Git 仓库,再进行 Husky 初始化。

image.png

安装相关依赖

这里husky只是可以hook到git的命令,具体要做什么,我们要自定义。
为了实现代码的自动化格式化和规范检查,我们需要安装 Prettier、ESLint,以及它们的重要辅助工具 lint-stagedeslint-config-prettier

# 安装 prettier 和 eslint (如果还没装的话)
npm install --save-dev prettier eslint

# 安装 lint-staged
npm install --save-dev lint-staged

# 解决 PrettierESLint 规则冲突的重要插件
# (这个插件会关闭所有可能与 Prettier 冲突的 ESLint 格式化规则)
npm install --save-dev eslint-config-prettier

配置lint-staged

在我们的 package.json 中添加 lint-staged 的配置。这告诉它对不同类型的文件执行什么操作:

{
  "scripts": {
    // ... 其他 scripts
  },
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": [
      "prettier --write",
      "eslint --fix"
    ],
    "*.{json,css,md,html}": [
      "prettier --write"
    ]
  },
  // ... 其他依赖
}

修改eslint.config.js配置

eslint-config-prettier 会关闭所有可能与 Prettier 格式规则冲突的 ESLint 规则。在 Flat Config 中,配置是按数组顺序应用的,后一个配置会覆盖前一个。因此,我们需要确保 eslintConfigPrettier 处于配置数组的末尾

// 1. 引入 prettier 配置 
import eslintConfigPrettier from "eslint-config-prettier";
// 2. 并在最后添加 eslintConfigPrettier
export default defineConfig([
  globalIgnores(["dist"]),
  {
    files: ["**/*.{ts,tsx}"],
    extends: [
      js.configs.recommended,
      tseslint.configs.recommended,
      reactHooks.configs.flat.recommended,
      reactRefresh.configs.vite,
    ],
    languageOptions: {
      ecmaVersion: 2020,
      globals: globals.browser,
    },
  },
  eslintConfigPrettier,
]);

修改husky的hook命令

这里就是最后的一步了,在commit的时候,我们要运行什么命令
修改.husky/pre-commit,在里面添加

npx lint-staged

自定义代码规范

如果我们想修改代码的格式规范,可以在prettierrc.json文件中对其进行修改,如果没有这个文件,创建一个就好了

// .prettierrc.json
{
  "tabWidth": 4,          // 缩进宽度为 4 个空格
  "semi": false,          // 不使用分号
  "singleQuote": true,    // 使用单引号
  "printWidth": 80,       // 每行代码最多 80 个字符 (可选,但推荐)
  "trailingComma": "all"  // 对象或数组的最后一个元素后总是添加逗号 (可选,但推荐)
}

总结

通过 Husky、Prettier、ESLint 和 lint-staged 的强强联合,我们构建了一个高效且强大的代码规范自动化流程。这将:

  • 统一代码风格: 团队成员的代码风格将高度一致,降低阅读成本。
  • 提升代码质量: 及时发现潜在的 Bug 和不规范的写法。
  • 优化开发体验: 格式化和检查在提交前自动完成,无需手动干预。
  • 确保项目健康: 从源头杜绝“屎山”的产生,让项目代码库始终保持健康和易于维护。

Vibe Coding 实战指南:从“手写代码”到“意图设计”的前端范式转移

1. 引言:零编码实现的初体验

最近在开发需求时,频繁听到关于 AI 编程能力的讨论。借着 Qoder 测试版发布的契机,我决定在实际业务中体验一把“零编码”开发。

这次实验不仅是一次工具的试用,更是一次开发模式的范式转移。我发现,在 AI 辅助下,前端工程师的角色正在发生本质变化:我们不再仅仅是代码的编写者(Coder),而是转变为**架构师 + 产品经理 + 质量保障(QA)**的复合体。这正是 Andrej Karpathy 所提出的 "Vibe Coding"(氛围编码/直觉编码) ——你负责把握逻辑与效果(The Vibe),AI 负责具体的语法实现(The Code)。

2. 实战复盘:级联多选组件的“无中生有”

2.1 需求背景与挑战

业务场景需要一个支持多选的级联选择器(省-市-区-社区),且交互逻辑复杂:

  • 省市层级单选,区与社区层级多选。
  • 单选与多选逻辑混合。
  • 技术难点:项目使用的 Vant UI 组件库中的 Cascader 组件原生仅支持单选。

2.2 初始沟通:粗糙但有效的意图传达

这是我第一次完全依赖 AI 编程。我的第一版 Prompt 非常直白,甚至有些粗糙,直接把需求和设计稿丢给了 AI:

User Prompt:
你知道 vant 中的 Cascader 吗?组件只能支持单选,我希望它支持多选。
请把这里的 Cascader 替换为多选组件。
我可以给你提供可供参考的 UI 设计稿,你有不懂的可以随时问我。
附件:ui稿位置@index.md

2.3 AI 的惊人反馈:不仅仅是代码

让我惊讶的是,Claude 并没有直接扔给我一堆代码,而是先生成了一份**《级联多选组件设计文档》**。

这份文档包含了:

  1. 技术架构:使用 Mermaid 绘制的组件层次结构图。
  2. 数据流向:清晰的 PropsEmit 接口定义。
  3. 逻辑边界:详细描述了“向下级联全选”和“向上级联反选”的逻辑。

💡 洞察:AI 生成的设计文档虽然完美,但实际生成的代码(MVP版本)却存在数据加载失败、Tab 无法切换等 Bug。这揭示了 Vibe Coding 的核心痛点:宏观设计完美,微观实现易错。

3. 核心方法论:如何让 AI 读懂你的“意图”

在经历了十几轮的修复和迭代后,我总结出了驾驭 AI 的三板斧。

3.1 意图定义(Define the Vibe)

放弃思考 DOM 结构,跳出传统的“实现细节”,转而描述“功能与交互”。

  • Bad Vibe: "写一个红色的按钮。"
  • Good Vibe: "创建一个主操作按钮,当鼠标 Hover 时有微小的缩放动画,点击时显示 Loading 状态,并且要符合我们现有的紫色品牌色调。"

3.2 规则约束(Context & Rules)

AI 就像一个才华横溢但不受约束的实习生。如果不立规矩,它会写出风格迥异的代码。我们需要通过 .cursorrules 或系统提示词来约束它。

实战经验: 我将团队的 ESLint 规则、TypeScript 规范以及 React/Vue 的最佳实践整理给 AI。例如:

  • 变量命名:必须使用 const,变量名要语义化。
  • 类型安全:禁止使用 any,优先使用 interface 而非 type
  • 样式管理:明确指定使用 Tailwind CSS 还是 styled-components,防止 AI 混用。

(注:在实际操作中,建议建立项目级的 .cursorrules 文件,将编码规范固化下来,AI 会自动读取。)

3.3 微调与迭代(Review & Refine)

AI 生成的代码往往不能直接上线,这时需要进行微调

  • 精确上下文:不要让 AI 盲目重写。选中具体的代码行,告诉它:“只修改这个函数的错误处理逻辑”。
  • 测试驱动:让 AI 自己生成测试用例和测试页面。我去测试页面操作,发现 Bug 后,将现象描述给 AI,让它自我修复。

4. 避坑指南与最佳实践

4.1 警惕“幻觉”与样式崩坏

  • 现象:AI 可能会编造不存在的 Tailwind 类名,或者使用了项目未安装的图标库。
  • 对策:在 Rules 中明确白名单。例如:“图标库请仅使用 Lucide-React,不要引入其他库。”

4.2 保持代码一致性

  • 现象:同一个组件,AI 一会儿用 function 定义,一会儿用箭头函数。
  • 对策One-Shot Learning(单样本学习) 。在 Prompt 中贴一段你认为完美的现有代码作为“范本”,告诉 AI:“请严格模仿这段代码的风格和结构来生成新组件。”

4.3 复杂状态管理的边界

  • 现象:当涉及复杂的全局状态(如 Redux/Zustand)或核心业务流时,AI 容易逻辑混乱。
  • 对策分层开发。核心的业务逻辑(Store 设计、API 层)建议由资深工程师把控架构,UI 层和简单的逻辑处理交给 AI。

5. 总结:前端工程师的进化

Vibe Coding 模式下,我们还需要写代码吗?答案是需要的,但写的“代码”变了

我们需要编写的不再是具体的 if-else,而是:

  1. Prompt:精准描述需求的自然语言。
  2. Rules:约束 AI 行为的规范文档。
  3. Tests:验证 AI 产出的验收标准。

在这个新时代,评估优秀前端的标准也随之改变:

  • Prompt Engineering 能力:能否用最短的语言描述最复杂的交互?
  • Code Review 能力:能否在 AI 生成的千行代码中,一眼洞察性能隐患?
  • 架构设计能力:能否搭建让 AI 发挥得更好的基础设施?

"Coding is not about typing; it's about thinking."

Vibe Coding 并没有消灭编程,它只是帮我们省去了“打字”的过程,让我们终于可以回归编程的本质:思考解决问题的方法

微前端qiankun接入的问题

改造的原因 产品由多个团队进行开发 包含:数据模型板块,数据治理板块,数据看板,持续集成等多个模块。 痛点 Git 冲突频繁 发布互相阻塞(要等某个模块发布完成后,才能继续发布) 技术栈无法升级(怕影

JS 对象深拷贝

创建深拷贝函数 这个函数将处理: 基本数据类型、对象和数组。 Date 和 RegExp 对象。 循环引用(防止无限递归导致栈溢出)。 Symbol 类型的属性。 函数和 undefined。 为什么

webWorker 初步体验

‌webWorker是什么? Web Worker是HTML5引入的浏览器多线程技术,允许在独立线程中执行后台任务,避免阻塞主线程从而提升Web应用性能。 为什么要使用Web Work? 浏览器渲染的
❌