普通视图

发现新文章,点击刷新页面。
昨天 — 2025年11月24日掘金 前端

kotlin-7

作者 风冷
2025年11月24日 20:44
好的,我们将继续向 Kotlin 的更深层次探索。这部分将涵盖编译器内部原理、语言设计哲学、高级类型系统以及与其他语言的互操作等专家级主题。 Kotlin 专家级探索:语言设计与实现原理 四十六、Ko

kotlin-6

作者 风冷
2025年11月24日 19:57
太棒了!现在让我们探索 Kotlin 的终极境界——元编程、编译器插件、深度优化和未来特性。这将带你进入 Kotlin 语言设计的核心领域。 Kotlin 终极探索:元编程与语言深度 三十九、注解处理

kotlin-5

作者 风冷
2025年11月24日 19:51

太棒了!现在让我们进入 Kotlin 更深入的高级特性和实际应用场景。这部分将涵盖泛型、注解、反射、DSL等企业级开发必备技能。


Kotlin 高级特性:企业级开发实战

三十二、泛型深入:型变、星投影与 reified

1. 型变(Variance)
// 不变(Invariant)- 默认行为
class Box<T>(val value: T)

fun main() {
    val stringBox = Box("Hello")
    // val anyBox: Box<Any> = stringBox // 错误!Box<String> 不是 Box<Any> 的子类型
}

// 协变(Covariant)- 使用 out 关键字
class ReadOnlyBox<out T>(val value: T) {
    fun get(): T = value
    // fun set(newValue: T) {} // 错误!不能有消费 T 的方法
}

// 逆变(Contravariant)- 使用 in 关键字
class WriteOnlyBox<in T> {
    fun set(value: T) {
        println("设置值: $value")
    }
    // fun get(): T // 错误!不能有生产 T 的方法
}

fun testVariance() {
    // 协变示例
    val stringBox: ReadOnlyBox<String> = ReadOnlyBox("Text")
    val anyBox: ReadOnlyBox<Any> = stringBox // 正确!因为 out 关键字
    
    // 逆变示例
    val anyWriteBox: WriteOnlyBox<Any> = WriteOnlyBox()
    val stringWriteBox: WriteOnlyBox<String> = anyWriteBox // 正确!因为 in 关键字
}
2. 星投影(Star Projection)
fun printSize(list: List<*>) {
    println("列表大小: ${list.size}")
    // println(list[0]) // 不能安全地访问元素内容
}

fun <T> compareFirstSecond(list: List<T>, comparator: Comparator<in T>): Boolean 
    where T : Comparable<T> {
    return if (list.size >= 2) {
        comparator.compare(list[0], list[1]) > 0
    } else false
}

// 使用 reified 关键字实现类型检查
inline fun <reified T> checkType(value: Any): Boolean {
    return value is T
}

fun main() {
    val list = listOf(1, 2, 3)
    printSize(list)
    
    println(checkType<String>("Hello")) // true
    println(checkType<Int>("Hello"))    // false
}

三十三、注解与反射

1. 自定义注解
// 定义注解
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class ApiVersion(val version: String)

@Target(AnnotationTarget.PROPERTY)
annotation class JsonExclude

@Target(AnnotationTarget.PROPERTY)
annotation class JsonName(val name: String)

// 使用注解
@ApiVersion("1.0")
data class User(
    @JsonName("user_name")
    val name: String,
    
    val age: Int,
    
    @JsonExclude
    val password: String
)

// 注解处理器模拟
fun processAnnotations(obj: Any) {
    val klass = obj::class
    val apiVersion = klass.annotations.find { it is ApiVersion } as? ApiVersion
    apiVersion?.let { println("API 版本: ${it.version}") }
    
    klass.memberProperties.forEach { prop ->
        prop.annotations.forEach { annotation ->
            when (annotation) {
                is JsonName -> println("属性 ${prop.name} 将被序列化为 ${annotation.name}")
                is JsonExclude -> println("属性 ${prop.name} 将被排除")
            }
        }
    }
}
2. 反射实战
import kotlin.reflect.full.*
import kotlin.reflect.jvm.isAccessible

class SecretClass {
    private val secretValue = "机密信息"
    private fun secretMethod() = "机密方法"
    
    public fun publicMethod() = "公开方法"
}

fun reflectExample() {
    val instance = SecretClass()
    val klass = instance::class
    
    println("=== 类信息 ===")
    println("类名: ${klass.simpleName}")
    println("成员属性: ${klass.memberProperties.map { it.name }}")
    println("成员函数: ${klass.memberFunctions.map { it.name }}")
    
    println("\n=== 访问私有成员 ===")
    // 访问私有属性
    val secretProperty = klass.memberProperties.find { it.name == "secretValue" }
    secretProperty?.let {
        it.isAccessible = true
        println("私有属性值: ${it.get(instance)}")
    }
    
    // 调用私有方法
    val secretMethod = klass.memberFunctions.find { it.name == "secretMethod" }
    secretMethod?.let {
        it.isAccessible = true
        println("私有方法结果: ${it.call(instance)}")
    }
}

三十四、类型安全的构建器(DSL)

1. HTML DSL 构建器
class HtmlElement(val name: String, val content: String = "") {
    private val children = mutableListOf<HtmlElement>()
    private val attributes = mutableMapOf<String, String>()
    
    fun attribute(name: String, value: String) {
        attributes[name] = value
    }
    
    fun child(element: HtmlElement) {
        children.add(element)
    }
    
    override fun toString(): String {
        val attrString = if (attributes.isNotEmpty()) {
            " " + attributes.entries.joinToString(" ") { "${it.key}="${it.value}"" }
        } else ""
        
        return if (children.isEmpty()) {
            "<$name$attrString>$content</$name>"
        } else {
            "<$name$attrString>${children.joinToString("")}$content</$name>"
        }
    }
}

// DSL 构建函数
fun html(block: HtmlBuilder.() -> Unit): HtmlElement {
    val builder = HtmlBuilder("html")
    builder.block()
    return builder.build()
}

class HtmlBuilder(private val rootName: String) {
    private val root = HtmlElement(rootName)
    private var currentElement: HtmlElement = root
    
    fun head(block: HeadBuilder.() -> Unit) {
        val headBuilder = HeadBuilder()
        headBuilder.block()
        root.child(headBuilder.build())
    }
    
    fun body(block: BodyBuilder.() -> Unit) {
        val bodyBuilder = BodyBuilder()
        bodyBuilder.block()
        root.child(bodyBuilder.build())
    }
    
    fun build(): HtmlElement = root
}

class HeadBuilder {
    private val head = HtmlElement("head")
    
    fun title(text: String) {
        head.child(HtmlElement("title", text))
    }
    
    fun build(): HtmlElement = head
}

class BodyBuilder {
    private val body = HtmlElement("body")
    
    fun h1(text: String, block: H1Builder.() -> Unit = {}) {
        val h1Builder = H1Builder(text)
        h1Builder.block()
        body.child(h1Builder.build())
    }
    
    fun p(text: String) {
        body.child(HtmlElement("p", text))
    }
    
    fun build(): HtmlElement = body
}

class H1Builder(private val text: String) {
    private val h1 = HtmlElement("h1", text)
    
    fun style(css: String) {
        h1.attribute("style", css)
    }
    
    fun build(): HtmlElement = h1
}

// 使用 DSL
fun createHtmlPage() {
    val page = html {
        head {
            title("我的网页")
        }
        body {
            h1("欢迎来到 Kotlin DSL") {
                style("color: blue;")
            }
            p("这是一个使用 DSL 构建的 HTML 页面")
        }
    }
    
    println(page)
}
2. 数据库查询 DSL
// 查询 DSL 定义
class QueryBuilder {
    private var table: String = ""
    private val conditions = mutableListOf<String>()
    private var limit: Int? = null
    private var orderBy: String? = null
    
    fun from(table: String) {
        this.table = table
    }
    
    fun where(condition: String) {
        conditions.add(condition)
    }
    
    fun limit(count: Int) {
        this.limit = count
    }
    
    fun orderBy(column: String) {
        this.orderBy = column
    }
    
    fun build(): String {
        val query = StringBuilder("SELECT * FROM $table")
        
        if (conditions.isNotEmpty()) {
            query.append(" WHERE ").append(conditions.joinToString(" AND "))
        }
        
        orderBy?.let {
            query.append(" ORDER BY $it")
        }
        
        limit?.let {
            query.append(" LIMIT $it")
        }
        
        return query.toString()
    }
}

// DSL 入口函数
fun query(block: QueryBuilder.() -> Unit): String {
    val builder = QueryBuilder()
    builder.block()
    return builder.build()
}

// 使用 DSL 构建查询
fun buildQueries() {
    val simpleQuery = query {
        from("users")
    }
    println("简单查询: $simpleQuery")
    
    val complexQuery = query {
        from("orders")
        where("status = 'completed'")
        where("amount > 100")
        orderBy("created_at DESC")
        limit(10)
    }
    println("复杂查询: $complexQuery")
}

三十五、合约(Contracts)与内联优化

import kotlin.contracts.*

// 使用合约优化智能转换
@OptIn(ExperimentalContracts::class)
fun String?.isNotNullOrEmpty(): Boolean {
    contract {
        returns(true) implies (this@isNotNullOrEmpty != null)
    }
    return this != null && this.isNotEmpty()
}

fun processText(text: String?) {
    if (text.isNotNullOrEmpty()) {
        // 由于合约,编译器知道 text 不为 null
        println(text.length) // 不需要安全调用
        println(text.uppercase())
    }
}

// 内联类 - 包装类型而不产生运行时开销
@JvmInline
value class Password(private val value: String) {
    init {
        require(value.length >= 8) { "密码必须至少8个字符" }
    }
    
    fun mask(): String = "*".repeat(value.length)
}

@JvmInline
value class UserId(val value: Int)

fun login(userId: UserId, password: Password) {
    println("用户 ${userId.value} 登录,密码: ${password.mask()}")
}

三十六、多平台项目(KMP)基础

// 公共模块 - 共享业务逻辑
expect class Platform() {
    val platform: String
}

class Greeting {
    private val platform = Platform()
    
    fun greet(): String {
        return "Hello from ${platform.platform}"
    }
}

// Android 实现
// androidMain/Platform.kt
actual class Platform actual constructor() {
    actual val platform: String = "Android"
}

// iOS 实现  
// iosMain/Platform.kt
actual class Platform actual constructor() {
    actual val platform: String = "iOS"
}

// 共享业务逻辑
class Calculator {
    fun add(a: Int, b: Int): Int = a + b
    fun multiply(a: Int, b: Int): Int = a * b
    
    fun calculateExpression(expression: String): Int {
        return when {
            expression.contains("+") -> {
                val parts = expression.split("+")
                add(parts[0].trim().toInt(), parts[1].trim().toInt())
            }
            expression.contains("*") -> {
                val parts = expression.split("*")
                multiply(parts[0].trim().toInt(), parts[1].trim().toInt())
            }
            else -> throw IllegalArgumentException("不支持的表达式")
        }
    }
}

三十七、性能优化与最佳实践

1. 集合操作优化
fun collectionOptimization() {
    val numbers = (1..1000000).toList()
    
    // 不好的写法:多次中间操作
    val badResult = numbers
        .filter { it % 2 == 0 }
        .map { it * 2 }
        .filter { it > 1000 }
        .take(10)
    
    // 好的写法:使用序列(惰性求值)
    val goodResult = numbers.asSequence()
        .filter { it % 2 == 0 }
        .map { it * 2 }
        .filter { it > 1000 }
        .take(10)
        .toList()
    
    println("结果: $goodResult")
}

// 使用 groupBy 优化复杂操作
data class Person(val name: String, val age: Int, val city: String)

fun optimizeGrouping() {
    val people = listOf(
        Person("Alice", 25, "Beijing"),
        Person("Bob", 30, "Shanghai"),
        Person("Charlie", 25, "Beijing"),
        Person("Diana", 30, "Shanghai")
    )
    
    // 按城市分组,然后按年龄分组
    val grouped = people.groupBy({ it.city }, { it.age })
    println("分组结果: $grouped")
    
    // 统计每个城市的平均年龄
    val averageAges = people.groupingBy { it.city }
        .fold(0.0) { accumulator, element -> 
            accumulator + element.age 
        }.mapValues { it.value / people.count { p -> p.city == it.key } }
    
    println("平均年龄: $averageAges")
}
2. 内存优化模式
// 使用对象池避免重复创建
class ConnectionPool private constructor() {
    private val available = mutableListOf<Connection>()
    private val inUse = mutableSetOf<Connection>()
    
    fun getConnection(): Connection {
        return available.removeFirstOrNull()?.also { inUse.add(it) } 
            ?: Connection().also { inUse.add(it) }
    }
    
    fun releaseConnection(connection: Connection) {
        inUse.remove(connection)
        available.add(connection)
    }
    
    companion object {
        val instance by lazy { ConnectionPool() }
    }
}

class Connection {
    fun connect() = println("连接建立")
    fun disconnect() = println("连接关闭")
}

// 使用 use 函数自动管理资源
fun readFileSafely() {
    // 模拟资源管理
    val resource = object : AutoCloseable {
        override fun close() = println("资源已释放")
        fun read() = "文件内容"
    }
    
    val content = resource.use { 
        it.read() 
    }
    println("内容: $content") // 资源会自动关闭
}

三十八、完整实战项目:构建事件总线系统

import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
import kotlinx.coroutines.flow.*
import kotlin.reflect.KClass

// 事件基类
sealed class Event {
    data class UserLoggedIn(val userId: String) : Event()
    data class OrderCreated(val orderId: String, val amount: Double) : Event()
    data class PaymentProcessed(val paymentId: String, val success: Boolean) : Event()
    object SystemShutdown : Event()
}

// 事件处理器接口
interface EventHandler<T : Event> {
    suspend fun handle(event: T)
}

// 事件总线
object EventBus {
    private val handlers = mutableMapOf<KClass<out Event>, MutableList<EventHandler<*>>>()
    private val eventChannel = Channel<Event>(Channel.UNLIMITED)
    
    // 注册事件处理器
    fun <T : Event> registerHandler(eventClass: KClass<T>, handler: EventHandler<T>) {
        handlers.getOrPut(eventClass) { mutableListOf() }.add(handler as EventHandler<*>)
    }
    
    // 发布事件
    fun publish(event: Event) {
        eventChannel.trySend(event)
    }
    
    // 启动事件处理循环
    fun start() = GlobalScope.launch {
        for (event in eventChannel) {
            processEvent(event)
        }
    }
    
    private suspend fun processEvent(event: Event) {
        val eventClass = event::class
        val eventHandlers = handlers[eventClass] ?: return
        
        eventHandlers.forEach { handler ->
            try {
                @Suppress("UNCHECKED_CAST")
                (handler as EventHandler<Event>).handle(event)
            } catch (e: Exception) {
                println("事件处理失败: ${e.message}")
            }
        }
    }
}

// 具体的事件处理器
class UserActivityLogger : EventHandler<Event.UserLoggedIn> {
    override suspend fun handle(event: Event.UserLoggedIn) {
        delay(100) // 模拟处理时间
        println("📝 用户登录日志: ${event.userId}${System.currentTimeMillis()} 登录")
    }
}

class OrderProcessor : EventHandler<Event.OrderCreated> {
    override suspend fun handle(event: Event.OrderCreated) {
        delay(200)
        println("💰 订单处理: 订单 ${event.orderId} 金额 ${event.amount}")
    }
}

class PaymentNotifier : EventHandler<Event.PaymentProcessed> {
    override suspend fun handle(event: Event.PaymentProcessed) {
        delay(150)
        val status = if (event.success) "成功" else "失败"
        println("📧 支付通知: 支付 ${event.paymentId} $status")
    }
}

class SystemMonitor : EventHandler<Event.SystemShutdown> {
    override suspend fun handle(event: Event.SystemShutdown) {
        println("🛑 系统关闭: 开始清理资源...")
        delay(500)
        println("🛑 系统关闭: 资源清理完成")
    }
}

// 使用事件总线
fun main() = runBlocking {
    // 注册处理器
    EventBus.registerHandler(Event.UserLoggedIn::class, UserActivityLogger())
    EventBus.registerHandler(Event.OrderCreated::class, OrderProcessor())
    EventBus.registerHandler(Event.PaymentProcessed::class, PaymentNotifier())
    EventBus.registerHandler(Event.SystemShutdown::class, SystemMonitor())
    
    // 启动事件总线
    EventBus.start()
    
    // 模拟发布事件
    EventBus.publish(Event.UserLoggedIn("user123"))
    EventBus.publish(Event.OrderCreated("order456", 99.99))
    EventBus.publish(Event.PaymentProcessed("pay789", true))
    EventBus.publish(Event.OrderCreated("order999", 49.99))
    EventBus.publish(Event.PaymentProcessed("pay000", false))
    
    delay(1000) // 等待事件处理完成
    
    EventBus.publish(Event.SystemShutdown())
    delay(1000)
    
    println("程序结束")
}

下一步学习方向

你现在已经掌握了 Kotlin 的企业级开发技能!接下来可以深入:

  1. Kotlin 编译器插件开发:自定义编译期处理
  2. Kotlin 元编程:在编译时生成代码
  3. Kotlin 与 Java 互操作高级技巧:类型映射、异常处理
  4. Kotlin 服务端开发:使用 Ktor、Spring Boot
  5. Kotlin 前端开发:使用 Compose for Web
  6. Kotlin 移动端开发:Compose Multiplatform

这些高级特性将让你在 Kotlin 开发中游刃有余,能够构建复杂、高性能的企业级应用!

🗣️面试官: 那些常见的前端面试场景问题

2025年11月24日 18:30

1. 页面白屏如何排查?

第一步:快速分类(30秒) "页面白屏主要有五种原因:JavaScript执行错误、资源加载失败、CSS样式问题、接口异常和浏览器兼容性。其中JavaScript错误最常见,特别是SPA应用中的未捕获异常。"

第二步:排查方法(1分钟) "我的排查步骤是:首先查看Console面板的错误信息,这能快速定位JS异常;然后检查Network面板确认资源加载状态;接着用Elements面板验证DOM和样式;移动端问题会用vConsole或真机调试。生产环境结合Sentry等监控系统分析。"

第三步:预防措施(30秒) "预防方面建立错误边界、资源容错机制、统一接口异常处理、兼容性检测,同时搭建监控告警体系。"

一、基础检测流程

1. 控制台检查(Console)

// 主动捕获全局错误(放在入口文件最前面)
window.addEventListener('error', function(event) {
  console.error('全局捕获:', event.error);
  // 可上报到监控系统
});

// 检查console是否有以下类型错误:
// - SyntaxError (语法错误)
// - TypeError (类型错误)
// - ReferenceError (引用错误)
// - 404资源加载失败

2. 网络请求检查(Network)

  • 关键指标

    • HTML文档状态码(200/304/404/500)
    • JS/CSS资源加载状态
    • 接口请求是否阻塞渲染
  • 检测示例

// 检查关键资源是否加载完成
const resourceCheck = () => {
  const entries = performance.getEntriesByType('resource');
  const criticalResources = entries.filter(entry => 
    entry.initiatorType === 'script' || 
    entry.initiatorType === 'css'
  );
  
  criticalResources.forEach(res => {
    if(res.responseStatus >= 400) {
      console.error(`资源加载失败: ${res.name}`, res);
    }
  });
};
window.addEventListener('load', resourceCheck);

二、深度检测方法

1. DOM渲染检测

// 检测DOM树是否正常构建
function checkDOMReady() {
  return new Promise((resolve) => {
    const check = () => {
      if(document.body && document.body.children.length > 0) {
        resolve(true);
      } else {
        setTimeout(check, 50);
      }
    };
    check();
  });
}

// 使用示例
checkDOMReady().then((isReady) => {
  if(!isReady) {
    console.error('DOM渲染超时');
    // 上报白屏信息
  }
});

2. 框架特定检测

Vue应用检测:
// 在main.js中添加
new Vue({
  render: h => h(App),
  errorCaptured(err, vm, info) {
    console.error('Vue组件错误:', err, info);
    // 可上报错误
    return false; // 阻止错误继续向上传播
  }
}).$mount('#app');

// 检查根组件挂载
if(!document.querySelector('#app').__vue__) {
  console.error('Vue根实例挂载失败');
}
React应用检测:
// Error Boundary组件
class ErrorBoundary extends React.Component {
  componentDidCatch(error, info) {
    console.error('React组件错误:', error, info);
    // 上报错误
  }
  
  render() {
    return this.props.children; 
  }
}

// 检查React根组件
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <ErrorBoundary>
    <App />
  </ErrorBoundary>
);

三、性能相关检测

1. 长任务检测

// 检测阻塞渲染的长时间任务
const observer = new PerformanceObserver((list) => {
  for(const entry of list.getEntries()) {
    if(entry.duration > 50) { // 超过50ms的任务
      console.warn('长任务影响渲染:', entry);
    }
  }
});
observer.observe({entryTypes: ["longtask"]});

2. 关键渲染路径监控

// 使用Performance API监控关键时间点
const perfData = window.performance.timing;
const metrics = {
  domReady: perfData.domComplete - perfData.domLoading,
  loadTime: perfData.loadEventEnd - perfData.navigationStart
};

if(metrics.domReady > 3000) {
  console.error('DOM解析时间过长:', metrics);
}

四、自动化检测方案

1. Puppeteer检测脚本

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  
  // 监听控制台错误
  page.on('console', msg => {
    if(msg.type() === 'error') {
      console.log('页面错误:', msg.text());
    }
  });
  
  // 设置超时检测
  await Promise.race([
    page.goto('https://your-site.com'),
    new Promise((_, reject) => 
      setTimeout(() => reject(new Error('页面加载超时')), 5000)
    )
  ]);
  
  // 检查可见内容
  const content = await page.evaluate(() => {
    return {
      bodyText: document.body.innerText,
      childCount: document.body.children.length
    };
  });
  
  if(content.childCount === 0 || content.bodyText.length < 10) {
    console.error('检测到白屏现象');
  }
  
  await browser.close();
})();

2. 真实用户监控(RUM)

// 使用浏览器的MutationObserver监控DOM变化
const observer = new MutationObserver((mutations) => {
  if(!document.querySelector('#app')?.innerHTML) {
    // 上报白屏事件
    beacon.send('white-screen', {
      url: location.href,
      ua: navigator.userAgent
    });
  }
});

observer.observe(document.body, {
  childList: true,
  subtree: true
});

五、常见白屏场景示例

  1. 资源加载失败

    <!-- 错误的资源路径 -->
    <script src="/wrong-path/app.js"></script>
    
  2. 语法错误

    // 缺少括号导致整个脚本不执行
    function test() {
      console.log('hello'
    }
    
  3. 框架初始化失败

    // Vue示例 - 挂载元素不存在
    new Vue({el: '#not-exist'});
    
  4. CSS阻塞

    <!-- 不正确的CSS引入阻塞渲染 -->
    <link rel="stylesheet" href="nonexist.css">
    
  5. 第三方库冲突

    // 两个库都修改了Array原型
    libraryA.modifyPrototype();
    libraryB.modifyPrototype(); // 冲突导致错误 ```
    

2.前端埋点

一、页面生命周期埋点

1. 页面加载阶段

// 记录页面开始加载时间
const pageStartTime = Date.now();

// 监听页面加载完成
window.addEventListener('load', () => {
  const loadTime = Date.now() - pageStartTime;
  track('page_load', {
    load_time: loadTime,
    referrer: document.referrer,
    resource_status: checkResources()
  });
});

// 检查关键资源加载状态
function checkResources() {
  return performance.getEntriesByType('resource').map(res => ({
    name: res.name,
    type: res.initiatorType,
    duration: res.duration.toFixed(2)
  }));
}

2. 用户交互阶段

// 点击事件埋点(支持事件委托)
document.body.addEventListener('click', (e) => {
  const target = e.target.closest('[data-track]');
  if(target) {
    track('element_click', {
      element_id: target.id,
      track_type: target.dataset.track,
      position: `${e.clientX},${e.clientY}`
    });
  }
});

// 滚动深度记录
let maxScroll = 0;
window.addEventListener('scroll', _.throttle(() => {
  const currentScroll = window.scrollY / document.body.scrollHeight;
  if(currentScroll > maxScroll) {
    maxScroll = currentScroll;
    track('scroll_depth', { depth: Math.round(maxScroll * 100) });
  }
}, 1000));

3. 页面停留时长计算

let activeStart = Date.now();
let inactiveTime = 0;

// 用户活跃状态检测
document.addEventListener('mousemove', resetActiveTimer);
document.addEventListener('keydown', resetActiveTimer);

function resetActiveTimer() {
  if(inactiveTime > 0) {
    track('user_inactive', { duration: inactiveTime });
    inactiveTime = 0;
  }
  activeStart = Date.now();
}

// 每10秒检测一次活跃状态
setInterval(() => {
  if(Date.now() - activeStart > 15000) { // 15秒无操作视为不活跃
    inactiveTime += 10000;
  } else {
    track('user_active', { duration: 10000 });
  }
}, 10000);

二、特殊场景处理

1. 页面隐藏/显示

// 页面可见性变化监听
document.addEventListener('visibilitychange', () => {
  if(document.hidden) {
    track('page_hide', { 
      stay_time: Date.now() - pageStartTime,
      scroll_depth: maxScroll
    });
  } else {
    track('page_show');
  }
});

2. 页面关闭前上报

// 确保页面关闭前数据上报
window.addEventListener('beforeunload', () => {
  const totalStay = Date.now() - pageStartTime;
  navigator.sendBeacon('/api/track', JSON.stringify({
    event: 'page_close',
    active_time: totalStay - inactiveTime,
    scroll_depth: maxScroll
  }));
});

三、数据上报优化方案

1. 批量上报机制

let eventQueue = [];
const MAX_QUEUE = 5;
const FLUSH_INTERVAL = 3000;

function addToQueue(event) {
  eventQueue.push(event);
  if(eventQueue.length >= MAX_QUEUE) {
    flushQueue();
  }
}

function flushQueue() {
  if(eventQueue.length === 0) return;
  
  const batchData = { batch: eventQueue };
  navigator.sendBeacon('/api/batch', JSON.stringify(batchData));
  eventQueue = [];
}

// 定时刷新队列
setInterval(flushQueue, FLUSH_INTERVAL);

2. 关键指标计算

// 计算FMP(首次有效绘制)
new PerformanceObserver((entryList) => {
  const [entry] = entryList.getEntriesByName('first-contentful-paint');
  track('fmp', { value: entry.startTime.toFixed(2) });
}).observe({type: 'paint', buffered: true});

// 计算LCP(最大内容绘制)
new PerformanceObserver((entryList) => {
  const entries = entryList.getEntries();
  const lastEntry = entries[entries.length - 1];
  track('lcp', { value: lastEntry.startTime.toFixed(2) });
}).observe({type: 'largest-contentful-paint', buffered: true});

四、面试回答精简版

"我们实现全链路埋点主要分三个阶段:

  1. 加载阶段

    • performance API采集DNS/TTFB等指标
    • 监听load事件记录完整加载时间
    • 检查关键资源状态(如图片/脚本)
  2. 交互阶段

    • 事件委托监听全局点击(带data-track属性)
    • 节流处理滚动事件计算最大深度
    • 通过mousemove/keydown检测活跃状态
  3. 离开阶段

    • visibilitychange处理页面切换
    • beforeunload+sendBeacon确保关闭前上报
    • 计算总停留时长和有效活跃时间

3.那为什么大家都使用请求 GIF 图片的方式上报埋点数据呢?

  • 防止跨域问题:前端监控的请求常常会遇到跨域问题,这可能会影响监控的准确性和可用性。然而,图片的src属性并不会跨域,因此使用GIF图片作为埋点可以正常发起请求,从而有效避免跨域问题。

  • 防止阻塞页面加载:在创建资源节点后,通常只有当对象注入到浏览器的DOM树后,浏览器才会实际发送资源请求。但反复操作DOM会引发性能问题,且载入js/css资源会阻塞页面渲染,从而影响用户体验。与此不同,构造图片打点不需要插入DOM,只要在js中new出Image对象就能发起请求,这样就不会有阻塞问题。即使在没有js的浏览器环境中,也能通过img标签正常打点,这是其他类型的资源请求所做不到的。

  • 体积小,节约流量:相比其他图片格式(如BMP和PNG),GIF图片具有更小的体积。例如,最小的BMP文件需要74个字节,PNG需要67个字节,而合法的GIF只需要43个字节。因此,使用GIF作为埋点可以显著节约流量,提高数据传输效率。

  • 浏览器支持性好:所有浏览器都支持Image对象,即使不支持XMLHttpRequest对象也一样。这意味着使用GIF进行埋点上报可以在各种浏览器环境中稳定运行。

  • 记录错误的过程很少出错:某些情况下,如Ajax通信过程中页面跳转,请求可能会被取消。但使用图片进行埋点上报则不会遇到这个问题,特别是在记录离开页面打点行为的时候会很有用。

120行代码,实现丝滑滚动的时间轴组件

作者 你不会困
2025年11月24日 18:30

前言

产品又看见一个非常高级的时间轴页面交互(对于前端来说,实现不难,主要是花时间),我们尝试一下让Trae 的solo来实现,最简洁的代码实现,尽可能的描述一下细节,这样才可以最准确的实现,少走弯路,不浪费我们宝贵的时间(有这时间多看几篇技术文章,不香?啊哈哈哈)

首先先搭建好一个项目,这里使用的是vite+vue3+ts,然后是对应的安装tailwind css,这些都是让Trae solo模式帮我们搭建一下,我们就不用从0搭建了

image-20251120152839739

先来看看最终的效果实现,你很难相信这是一次对话就实现的

image.png 一.首先是描述细节

1.时间轴的中间是可以滚动的,然后要根据鼠标进行滚动,不要滚动条

2.奇数是在上面,时间轴的连接线比较长,偶数是在下面,连接线比较短

3.箭头在右下角固定,保证用户知道这个是时间轴

4.卡片最大宽度是240px,超出隐藏

5.线和卡片中间使用倒三角连接起来

附加上一张ui设计的截图,就可以开始提问了

image.png

等待了一会,就实现了

来看看代码,119行就实现了,看起来确实优雅,解读一下代码trae solo是如何实现这个组件的

1.卡片(奇偶项交错,下移以视觉区分),使用tailwind的mt-[210px],

<div
          class="relative max-w-[230px] overflow-hidden rounded-lg bg-[#1D2129] shadow-lg"
          :class="i % 2 !== 0 ? 'mt-[210px]' : ''"
        >
          <!-- 卡片头部:深色背景 -->
          <div class="flex items-center justify-between px-3 py-2">
            <span class="text-sm font-medium text-white">共7条</span>
            <span class="cursor-pointer text-xs text-white">全部></span>
          </div>

          <!-- 卡片内容:两张图片并排 -->
          <div class="flex gap-2 overflow-hidden p-3">
            <div class="relative shrink-0 overflow-hidden rounded">
              <img :src="item.image" class="aspect-video h-[150px] w-[89px] object-cover" alt="">
            </div>
            <div class="relative shrink-0 overflow-hidden rounded">
              <img :src="item.image" class="aspect-video h-[150px] w-[89px] object-cover" alt="">
            </div>
            <div class="relative shrink-0 overflow-hidden rounded">
              <img :src="item.image" class="aspect-video h-[150px] w-[89px] object-cover" alt="">
            </div>
          </div>
        </div>

2.底部小三角形指示器,有没有勾起你当时学前端的回忆,那道经典的面试题的,你是怎么实现一个三角形的

<div class="border-x-[12px] border-t-[12px] border-[#1D2129] border-x-transparent" />

3.竖线:奇数项加长(视觉第1、3、5项),使用tailwind的h-[310px]和h-[101px]来实现连接线的长短

<div
          class="w-[2px]" :class="[            isCreation ? 'bg-blue-500' : 'bg-[#685FE1]',            i % 2 === 0 ? 'h-[310px]' : 'h-[101px]',          ]"
        />

4.底部圆点的实现,也是使用一个div来实现的

<div
    class="z-10 size-4 rounded-full border-[3px] bg-white" :class="[    isCreation ? 'border-blue-500' : 'border-[#685FE1]',    ]"
/>

5.底部横向主线(黑色)贯穿整个容器

<div class="absolute inset-x-0 bottom-[35px] h-[2px] bg-black"/>

6.底部时间轴右侧箭头(固定在轴线右端,靠内边距 right-[-11px] 可调)

  <div class="pointer-events-none absolute bottom-[56px] right-[-11px] z-[999]">
    <!-- 向右的箭头 SVG,样式可改 -->
    <svg class="size-6 text-black" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
      <path d="M5 12h13" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" />
      <path d="M13 5l7 7-7 7" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" />
    </svg>
  </div>

关键代码,通过监听鼠标滚轮来实现横向滚动的

onMounted(() => {
  const el = scrollRef.value
  if (!el)
    return
  // 鼠标滚轮横向滚动
  el.addEventListener('wheel', (e) => {
    if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) {
      e.preventDefault()
      el.scrollLeft += e.deltaY
    }
  })
})

来看看代码行数,确实很惊艳,简单的119行就实现了 image.png

总结

trae的solo确实比之前好很多,也惊艳到我了,前端可真是越来越难了,以后的时间排期也会进一步的压缩了,还是要适当的使用ai来辅助我们的编程,把一些比较容易实现的,耗时间的工作交给ai去实现,我们专注于一些工程化和架构上面去,也可以适当学一些node项目,提高我们自己的核心竞争力,加油吧,前端工程师们。

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

作者 San30
2025年11月24日 18:21

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

作者 San30
2025年11月24日 18:05

在现代 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 日历组件完全指南:从网格生成到农历转换

作者 少卿
2025年11月24日 18:00

本文详细介绍如何从零实现一个功能完整的 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摇树的原理

2025年11月24日 17:58

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 图标

作者
2025年11月24日 17:26

前言: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日 17:24

日期: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 打造前端项目的代码基石

2025年11月24日 17:13

一个多人参与的项目,如果没有代码规范,会导致代码样式五花八门,特别难看。 我相信对于程序员这个职业来说,一个项目中,代码格式千奇百怪,没有几个人能忍吧。
现在在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 实战指南:从“手写代码”到“意图设计”的前端范式转移

2025年11月24日 16:45

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

❌
❌