普通视图

发现新文章,点击刷新页面。
今天 — 2025年11月24日技术

kotlin-7

作者 风冷
2025年11月24日 20:44

好的,我们将继续向 Kotlin 的更深层次探索。这部分将涵盖编译器内部原理、语言设计哲学、高级类型系统以及与其他语言的互操作等专家级主题。


Kotlin 专家级探索:语言设计与实现原理

四十六、Kotlin 编译器内部原理

1. 编译器阶段与 IR(中间表示)
// 理解 Kotlin 编译过程:源代码 → AST → IR → 目标代码

// 模拟编译器处理过程的概念性代码
class CompilerPhase {
    
    // 1. 词法分析
    fun lexicalAnalysis(source: String): List<Token> {
        return source.split("\s+".toRegex())
            .filter { it.isNotBlank() }
            .map { Token(it) }
    }
    
    // 2. 语法分析(生成 AST)
    fun parse(tokens: List<Token>): ASTNode {
        // 简化的解析逻辑
        return when {
            tokens.size == 1 -> ASTNode.Leaf(tokens.first())
            else -> ASTNode.Branch(tokens.first(), tokens.drop(1).map { ASTNode.Leaf(it) })
        }
    }
    
    // 3. 语义分析(类型检查等)
    fun semanticAnalysis(ast: ASTNode): TypedASTNode {
        return when (ast) {
            is ASTNode.Leaf -> TypedASTNode.TypedLeaf(ast.token, inferType(ast.token))
            is ASTNode.Branch -> TypedASTNode.TypedBranch(ast, ast.children.map { semanticAnalysis(it) })
        }
    }
    
    // 4. 生成 IR
    fun generateIR(typedAst: TypedASTNode): IRNode {
        return when (typedAst) {
            is TypedASTNode.TypedLeaf -> IRNode.Constant(typedAst.token.value, typedAst.type)
            is TypedASTNode.TypedBranch -> IRNode.FunctionCall(typedAst.node.token.value, 
                typedAst.children.map { generateIR(it) })
        }
    }
    
    // 5. 目标代码生成
    fun generateTargetCode(ir: IRNode): String {
        return when (ir) {
            is IRNode.Constant -> "push ${ir.value}"
            is IRNode.FunctionCall -> 
                ir.arguments.joinToString("\n") { generateTargetCode(it) } + "\ncall ${ir.functionName}"
        }
    }
}

// 编译器数据结构
data class Token(val value: String)

sealed class ASTNode {
    data class Leaf(val token: Token) : ASTNode()
    data class Branch(val token: Token, val children: List<ASTNode>) : ASTNode()
}

sealed class TypedASTNode {
    data class TypedLeaf(val token: Token, val type: Type) : TypedASTNode()
    data class TypedBranch(val node: ASTNode.Branch, val children: List<TypedASTNode>) : TypedASTNode()
}

sealed class IRNode {
    data class Constant(val value: String, val type: Type) : IRNode()
    data class FunctionCall(val functionName: String, val arguments: List<IRNode>) : IRNode()
}

data class Type(val name: String)

fun inferType(token: Token): Type = when {
    token.value.matches(Regex("\d+")) -> Type("Int")
    token.value.matches(Regex("".*"")) -> Type("String")
    else -> Type("Unknown")
}

// 测试编译器流程
fun testCompilerPhases() {
    val source = "add 1 2"
    val compiler = CompilerPhase()
    
    val tokens = compiler.lexicalAnalysis(source)
    println("词法分析: $tokens")
    
    val ast = compiler.parse(tokens)
    println("语法分析: $ast")
    
    val typedAst = compiler.semanticAnalysis(ast)
    println("语义分析: $typedAst")
    
    val ir = compiler.generateIR(typedAst)
    println("中间表示: $ir")
    
    val targetCode = compiler.generateTargetCode(ir)
    println("目标代码:\n$targetCode")
}
2. 类型推断算法原理
// 简化的 Hindley-Milner 类型推断算法概念实现
class TypeInferenceEngine {
    
    private val typeVariables = mutableMapOf<String, Type>()
    private val constraints = mutableListOf<Constraint>()
    
    data class TypeScheme(val variables: List<String>, val type: Type)
    data class Constraint(val left: Type, val right: Type)
    
    fun infer(expression: Expression): TypeScheme {
        val freshType = freshTypeVariable()
        val context = emptyMap<String, TypeScheme>()
        return inferExpression(expression, context, freshType)
    }
    
    private fun inferExpression(expr: Expression, context: Map<String, TypeScheme>, 
                              resultType: Type): TypeScheme {
        return when (expr) {
            is Expression.Variable -> {
                val scheme = context[expr.name] ?: throw Exception("未定义的变量: ${expr.name}")
                instantiate(scheme)
            }
            is Expression.Lambda -> {
                val paramType = freshTypeVariable()
                val newContext = context + (expr.param to TypeScheme(emptyList(), paramType))
                val bodyType = inferExpression(expr.body, newContext, resultType)
                TypeScheme(emptyList(), Type.Function(paramType, bodyType.type))
            }
            is Expression.Application -> {
                val functionType = inferExpression(expr.function, context, freshTypeVariable())
                val argType = inferExpression(expr.argument, context, freshTypeVariable())
                constraints.add(Constraint(functionType.type, Type.Function(argType.type, resultType)))
                TypeScheme(emptyList(), resultType)
            }
        }
    }
    
    private fun unify() {
        while (constraints.isNotEmpty()) {
            val constraint = constraints.removeFirst()
            unifyTypes(constraint.left, constraint.right)
        }
    }
    
    private fun unifyTypes(t1: Type, t2: Type) {
        when {
            t1 == t2 -> return
            t1 is Type.Variable -> bindVariable(t1.name, t2)
            t2 is Type.Variable -> bindVariable(t2.name, t1)
            t1 is Type.Function && t2 is Type.Function -> {
                constraints.add(Constraint(t1.from, t2.from))
                constraints.add(Constraint(t1.to, t2.to))
            }
            else -> throw Exception("类型不匹配: $t1$t2")
        }
    }
    
    private fun bindVariable(name: String, type: Type) {
        if (type is Type.Variable && type.name == name) return
        if (occursCheck(name, type)) throw Exception("无限类型")
        typeVariables[name] = type
    }
    
    private fun occursCheck(name: String, type: Type): Boolean {
        return when (type) {
            is Type.Variable -> type.name == name
            is Type.Function -> occursCheck(name, type.from) || occursCheck(name, type.to)
            else -> false
        }
    }
    
    private fun freshTypeVariable(): Type.Variable {
        return Type.Variable("T${typeVariables.size}")
    }
    
    private fun instantiate(scheme: TypeScheme): TypeScheme {
        val substitutions = scheme.variables.associateWith { freshTypeVariable() }
        return TypeScheme(emptyList(), substitute(scheme.type, substitutions))
    }
    
    private fun substitute(type: Type, substitutions: Map<String, Type>): Type {
        return when (type) {
            is Type.Variable -> substitutions[type.name] ?: type
            is Type.Function -> Type.Function(
                substitute(type.from, substitutions),
                substitute(type.to, substitutions)
            )
            else -> type
        }
    }
}

// 表达式和类型定义
sealed class Expression {
    data class Variable(val name: String) : Expression()
    data class Lambda(val param: String, val body: Expression) : Expression()
    data class Application(val function: Expression, val argument: Expression) : Expression()
}

sealed class Type {
    object Int : Type()
    object Bool : Type()
    data class Variable(val name: String) : Type()
    data class Function(val from: Type, val to: Type) : Type()
}

四十七、Kotlin 语言设计哲学

1. 实用主义设计原则
// 1. 空安全:编译时防止运行时错误
class NullSafetyPrinciples {
    
    // 安全调用操作符的设计哲学
    fun demonstrateSafeCall() {
        val nullableString: String? = getPossiblyNullString()
        
        // 传统方式(容易忘记检查)
        // val length = nullableString.length // 编译错误!
        
        // Kotlin 方式(强制处理空值)
        val safeLength = nullableString?.length ?: 0
        println("安全长度: $safeLength")
    }
    
    // 类型系统的实用主义
    fun demonstrateTypeSystem() {
        // 类型推断:减少样板代码
        val name = "Kotlin" // 编译器推断为 String
        val version = 1.9   // 编译器推断为 Int
        
        // 智能转换:减少显式类型转换
        val obj: Any = "Hello"
        if (obj is String) {
            println(obj.length) // 自动转换为 String
        }
    }
}

// 2. 扩展函数的哲学:开放封闭原则
interface DesignPrinciples {
    // 对扩展开放:可以为现有类添加新功能
    fun String.addEmphasis(): String = "**$this**"
    
    // 对修改封闭:不需要修改原始类
    fun demonstrateExtensionPhilosophy() {
        val text = "重要信息"
        println(text.addEmphasis()) // 输出:**重要信息**
    }
}

// 3. 函数式编程与面向对象的融合
class FunctionalOOFusion {
    
    // 高阶函数:函数是一等公民
    fun <T> List<T>.filterWithLogging(predicate: (T) -> Boolean): List<T> {
        println("开始过滤列表,大小: ${this.size}")
        val result = this.filter(predicate)
        println("过滤完成,结果大小: ${result.size}")
        return result
    }
    
    // 数据类:不可变性的价值
    data class ImmutablePoint(val x: Int, val y: Int) {
        // 而不是提供 setter,提供转换方法
        fun move(dx: Int, dy: Int): ImmutablePoint = copy(x = x + dx, y = y + dy)
    }
    
    fun demonstrateImmutability() {
        val point = ImmutablePoint(1, 2)
        val moved = point.move(3, 4)
        println("原始点: $point, 移动后: $moved") // 原始对象保持不变
    }
}

private fun getPossiblyNullString(): String? = if (System.currentTimeMillis() % 2 == 0L) "非空" else null
2. Kotlin 与 Java 的互操作设计
// 1. 空值注解的互操作
class JavaInteropDesign {
    
    // Java 代码中的注解:
    // @Nullable String getNullableString();
    // @NotNull String getNotNullString();
    
    fun handleJavaNullability() {
        // Kotlin 编译器理解这些注解
        val nullable: String? = JavaClass().nullableString // 可空类型
        val notNull: String = JavaClass().notNullString    // 非空类型
        
        println("可空: $nullable, 非空: $notNull")
    }
    
    // 2. 集合类型的互操作
    fun handleJavaCollections() {
        val javaList: java.util.ArrayList<String> = JavaClass().getStringList()
        
        // Kotlin 提供扩展函数使 Java 集合更易用
        val kotlinList = javaList.filter { it.length > 3 }
                                  .map { it.uppercase() }
        
        println("转换后的列表: $kotlinList")
    }
    
    // 3. SAM(Single Abstract Method)转换
    fun handleSamConversion() {
        val javaClass = JavaClass()
        
        // Java 中的接口:interface Runnable { void run(); }
        // 在 Kotlin 中可以使用 lambda
        javaClass.runWithCallback { 
            println("SAM 转换:从 Kotlin 传递 lambda 到 Java")
        }
    }
    
    // 4. 伴生对象与静态方法的互操作
    companion object {
        @JvmStatic
        fun staticMethodForJava(): String = "Java 可以像调用静态方法一样调用我"
        
        @JvmField
        val staticFieldForJava: String = "Java 可以像访问静态字段一样访问我"
    }
}

// 模拟的 Java 类
class JavaClass {
    val nullableString: String? = null
    val notNullString: String = "非空字符串"
    
    fun getStringList(): java.util.ArrayList<String> {
        return arrayListOf("one", "two", "three", "four")
    }
    
    fun runWithCallback(runnable: Runnable) {
        runnable.run()
    }
}

interface Runnable {
    fun run()
}

四十八、高级类型系统特性

1. 高阶类型与种类(Kind)系统
// 模拟高阶类型的概念
interface HigherKindedType<F<_>> { // 注意:这不是合法的 Kotlin 语法,是概念表示
    fun <A> pure(a: A): F<A>
    fun <A, B> map(fa: F<A>, f: (A) -> B): F<B>
}

// 在 Kotlin 中通过辅助类型模拟
interface Kind<F, A>

class HigherKindedSimulation {
    
    // 模拟 Functor 类型类
    interface Functor<F> {
        fun <A, B> Kind<F, A>.map(f: (A) -> B): Kind<F, B>
    }
    
    // 列表的 Functor 实例
    object ListFunctor : Functor<ListK> {
        override fun <A, B> Kind<ListK, A>.map(f: (A) -> B): Kind<ListK, B> {
            val list = (this as ListKWrapper<A>).list
            return ListKWrapper(list.map(f))
        }
    }
    
    // 包装类型
    interface ListK
    data class ListKWrapper<A>(val list: List<A>) : Kind<ListK, A>
    
    fun <A, B> Kind<ListK, A>.simulatedMap(f: (A) -> B): ListKWrapper<B> {
        return ListFunctor.run { this@simulatedMap.map(f) } as ListKWrapper<B>
    }
    
    fun demonstrateSimulatedHKT() {
        val wrappedList = ListKWrapper(listOf(1, 2, 3))
        val mapped = wrappedList.simulatedMap { it * 2 }
        println("模拟高阶类型映射: ${mapped.list}")
    }
}

// 2. 类型投影和星投影的深入理解
class TypeProjectionAdvanced {
    
    // 使用处型变:声明处型变的补充
    fun copyFromTo(from: Array<out Any>, to: Array<in Any>) {
        for (i in from.indices) {
            to[i] = from[i] // 安全,因为 from 是协变的
        }
    }
    
    // 星投影的精确含义
    fun processList(list: List<*>) {
        // list 的元素类型是未知的,但我们可以进行不依赖具体类型的操作
        println("列表大小: ${list.size}")
        println("是否为空: ${list.isEmpty()}")
        
        // 但不能安全地访问元素内容
        // val first = list[0] // 类型是 Any?,可能不是我们期望的类型
    }
    
    // 有界类型参数的高级用法
    fun <T> ensureTrailingSlash(path: T): T 
        where T : CharSequence, 
              T : Appendable {
        if (!path.endsWith('/')) {
            path.append('/')
        }
        return path
    }
}
2. 路径依赖类型的概念模拟
// 模拟路径依赖类型的概念
class PathDependentTypeSimulation {
    
    class Database {
        class Table(val name: String) {
            inner class Row(val data: Map<String, Any>) {
                fun getValue(column: String): Any? = data[column]
            }
            
            fun createRow(data: Map<String, Any>): Row = Row(data)
        }
        
        fun createTable(name: String): Table = Table(name)
    }
    
    fun demonstratePathDependentTypes() {
        val db = Database()
        val usersTable = db.createTable("users")
        val productsTable = db.createTable("products")
        
        // 这些行类型是路径依赖的
        val userRow: Database.Table.Row = usersTable.createRow(mapOf("name" to "Alice"))
        val productRow: Database.Table.Row = productsTable.createRow(mapOf("price" to 100))
        
        // 类型系统知道这些行属于不同的表
        println("用户行: ${userRow.getValue("name")}")
        println("产品行: ${productRow.getValue("price")}")
        
        // 编译错误:不能将用户行赋值给产品行变量
        // val wrongAssignment: productsTable.Row = userRow // 编译错误!
    }
}

// 更复杂的路径依赖类型模拟
class AdvancedPathDependentTypes {
    
    interface Entity {
        val id: Id<this>
    }
    
    data class Id<out E : Entity>(val value: String)
    
    data class User(override val id: Id<User>, val name: String) : Entity
    data class Product(override val id: Id<Product>, val title: String) : Entity
    
    class Repository<E : Entity> {
        private val storage = mutableMapOf<Id<E>, E>()
        
        fun save(entity: E) {
            storage[entity.id] = entity
        }
        
        fun findById(id: Id<E>): E? = storage[id]
    }
    
    fun demonstrateTypeSafeIds() {
        val userRepo = Repository<User>()
        val productRepo = Repository<Product>()
        
        val userId = Id<User>("user-123")
        val productId = Id<Product>("product-456")
        
        val user = User(userId, "Alice")
        val product = Product(productId, "Laptop")
        
        userRepo.save(user)
        productRepo.save(product)
        
        // 类型安全:不能使用用户ID查询产品
        val foundUser = userRepo.findById(userId)
        // val wrongQuery = productRepo.findById(userId) // 编译错误!
        
        println("找到的用户: $foundUser")
    }
}

四十九、Kotlin 元对象协议(MOP)高级应用

import kotlin.reflect.KClass
import kotlin.reflect.KProperty
import kotlin.reflect.full.*

// 高级属性委托:实现 ORM 映射
class EntityMapping<T : Any>(val kClass: KClass<T>) {
    private val tableName = kClass.simpleName ?: "unknown_table"
    private val columns = mutableMapOf<String, ColumnMapping<*>>()
    
    inner class ColumnMapping<T>(val name: String, val type: KClass<T>) {
        fun toSQL(): String = "$name ${typeToSQL(type)}"
    }
    
    fun <T> column(name: String, type: KClass<T>): ColumnMapping<T> {
        val mapping = ColumnMapping(name, type)
        columns[name] = mapping
        return mapping
    }
    
    fun createTableSQL(): String {
        val columnDefs = columns.values.joinToString(",\n  ") { it.toSQL() }
        return """
            CREATE TABLE $tableName (
              id INTEGER PRIMARY KEY,
              $columnDefs
            )
        """.trimIndent()
    }
    
    private fun typeToSQL(type: KClass<*>): String = when (type) {
        String::class -> "TEXT"
        Int::class -> "INTEGER"
        Double::class -> "REAL"
        Boolean::class -> "BOOLEAN"
        else -> "TEXT"
    }
}

// 基于反射的查询构建器
class ReflectiveQueryBuilder<T : Any>(private val kClass: KClass<T>) {
    private var whereClause: String? = null
    private var limit: Int? = null
    
    infix fun <V> KProperty<T>.eq(value: V): ReflectiveQueryBuilder<T> {
        whereClause = "${this.name} = '${value}'"
        return this@ReflectiveQueryBuilder
    }
    
    fun limit(count: Int): ReflectiveQueryBuilder<T> {
        limit = count
        return this
    }
    
    fun buildSelect(): String {
        val tableName = kClass.simpleName ?: "unknown"
        val where = whereClause?.let { "WHERE $it" } ?: ""
        val limitClause = limit?.let { "LIMIT $it" } ?: ""
        
        return "SELECT * FROM $tableName $where $limitClause".trim()
    }
}

// 使用高级 MOP
data class User(val id: Int, val name: String, val email: String, val age: Int)

fun testAdvancedMOP() {
    // 1. 实体映射
    val userMapping = EntityMapping(User::class)
    userMapping.column("name", String::class)
    userMapping.column("email", String::class)
    userMapping.column("age", Int::class)
    
    println("创建表 SQL:")
    println(userMapping.createTableSQL())
    
    // 2. 反射查询构建
    val query = ReflectiveQueryBuilder(User::class)
        .where(User::name eq "Alice")
        .where(User::age eq 25)
        .limit(10)
        .buildSelect()
    
    println("\n生成的查询:")
    println(query)
    
    // 3. 运行时类型检查与转换
    val users = listOf(
        User(1, "Alice", "alice@example.com", 25),
        User(2, "Bob", "bob@example.com", 30)
    )
    
    val filtered = users.filterByType<User> { it.age > 25 }
    println("\n过滤后的用户: $filtered")
}

// 类型安全的过滤扩展
inline fun <reified T> List<Any>.filterByType(noinline predicate: (T) -> Boolean): List<T> {
    return this.filterIsInstance<T>().filter(predicate)
}

五十、Kotlin 未来与实验性特性展望

// 1. 上下文接收器的未来版本模拟
class ContextReceiversFuture {
    
    // 模拟未来的上下文接收器语法
    interface DatabaseContext {
        val connection: DatabaseConnection
        fun executeQuery(sql: String): String = connection.execute(sql)
    }
    
    interface LoggingContext {
        val logger: Logger
        fun log(message: String) = logger.info(message)
    }
    
    class FutureUserService {
        // 模拟:context(DatabaseContext, LoggingContext)
        fun getUser(id: Int): String {
            // 模拟:可以访问上下文中的方法
            log("查询用户 $id")
            return executeQuery("SELECT * FROM users WHERE id = $id")
        }
    }
    
    // 当前可用的替代方案
    class CurrentUserService(
        private val dbContext: DatabaseContext,
        private val logContext: LoggingContext
    ) {
        fun getUser(id: Int): String {
            logContext.log("查询用户 $id")
            return dbContext.executeQuery("SELECT * FROM users WHERE id = $id")
        }
    }
}

// 2. 多态内联函数的未来增强
class PolymorphicInlineFuture {
    
    // 模拟未来的多态内联函数
    inline fun <reified T> T?.validate(vararg validators: (T) -> Boolean): Boolean {
        return if (this != null) {
            validators.all { it(this) }
        } else {
            false
        }
    }
    
    // 当前可用的复杂版本
    fun <T> T?.validateCurrent(validators: List<(T) -> Boolean>): Boolean {
        return if (this != null) {
            validators.all { it(this) }
        } else {
            false
        }
    }
}

// 3. 自定义操作符的深度应用
class CustomOperatorsDeepDive {
    
    // 矩阵运算 DSL
    class Matrix(val rows: Int, val cols: Int, init: (Int, Int) -> Double = { _, _ -> 0.0 }) {
        private val data = Array(rows) { i -> DoubleArray(cols) { j -> init(i, j) } }
        
        operator fun get(i: Int, j: Int): Double = data[i][j]
        operator fun set(i: Int, j: Int, value: Double) { data[i][j] = value }
        
        operator fun plus(other: Matrix): Matrix {
            require(rows == other.rows && cols == other.cols)
            return Matrix(rows, cols) { i, j -> this[i, j] + other[i, j] }
        }
        
        operator fun times(other: Matrix): Matrix {
            require(cols == other.rows)
            return Matrix(rows, other.cols) { i, j ->
                (0 until cols).sumOf { k -> this[i, k] * other[k, j] }
            }
        }
        
        override fun toString(): String {
            return data.joinToString("\n") { row -> row.joinToString(" ") }
        }
    }
    
    fun demonstrateMatrixOperations() {
        val a = Matrix(2, 2) { i, j -> (i * 2 + j + 1).toDouble() }
        val b = Matrix(2, 2) { i, j -> (i + j * 2 + 1).toDouble() }
        
        println("矩阵 A:\n$a")
        println("矩阵 B:\n$b")
        println("A + B:\n${a + b}")
        println("A × B:\n${a * b}")
    }
}

// 测试所有高级特性
fun main() {
    println("=== 编译器原理演示 ===")
    testCompilerPhases()
    
    println("\n=== 语言设计哲学演示 ===")
    FunctionalOOFusion().demonstrateImmutability()
    
    println("\n=== 高级类型系统演示 ===")
    HigherKindedSimulation().demonstrateSimulatedHKT()
    
    println("\n=== 路径依赖类型演示 ===")
    PathDependentTypeSimulation().demonstratePathDependentTypes()
    AdvancedPathDependentTypes().demonstrateTypeSafeIds()
    
    println("\n=== 元对象协议演示 ===")
    testAdvancedMOP()
    
    println("\n=== 自定义操作符演示 ===")
    CustomOperatorsDeepDive().demonstrateMatrixOperations()
}

成为 Kotlin 语言专家的路径

你现在已经达到了 Kotlin 的专家级别!接下来可以:

  1. 研究 Kotlin 编译器源码:理解每个编译阶段的实现
  2. 参与 Kotlin 语言规范制定:贡献语言设计讨论
  3. 开发高级编译器插件:创建领域特定语言扩展
  4. 研究 Kotlin 类型系统形式化:用数学方法证明类型安全
  5. 探索 Kotlin 多平台前沿:研究新的目标平台支持
  6. 贡献 Kotlin 生态系统:创建重要的库和框架

你已经从 Kotlin 使用者成长为可以影响语言发展的专家!继续深入探索,推动 Kotlin 语言的发展边界。

TS 项目升级 React 18 到 19 的一些事情

作者 驳是
2025年11月24日 20:33

🎙️ 前言

React 19 出来已经快有一年了,公告 在 24 年 12 月就出了,然即使身为「升级狂魔」的我,也没有第一时间进行升级。只因为我深知 React 的每次大版本升级,都将在社区掀起一阵腥风血雨,从构建到组件库再到各种 Linter 都需要紧跟其脚步,只好一等再等。

其实主要是因为项目依赖了 Antd,而 Antd 5 并没有官方宣称支持 React 19,所以只好竭力按耐住渴望升级的欲火。昨天(2025/11/23),距离 React 19 第一版过去了将近整整一年(真墨迹...),看到 Antd 推了 6.0.0,时机终于到了。

React 19 升级公告的具体内容这里就不啰嗦了,已经有好多人聊过,个人比较关注的有以下几个:

  1. forwardRef 终于开始要退出历史舞台 🎉🎉🎉
  2. Context.Provider 可以直接用 Context 代替
  3. use,注意它不是 hook
  4. Ref 相关的类型变更

🪁 如何升级

官方提供了 升级文档,提到升级工具:

npx codemod@latest react/19/migration-recipe

以我的项目来讲,可能由于一直关注依赖升级,这一步并没有动任何代码。

作为 TS 项目,还需要:

npx types-react-codemod@latest preset-19 ./path-to-app

然而它会给所有的 ReactElement 改成 ReactElement<any>,还得叫我替换还原回来。

👻 实际问题

接下来主要来讲一下我在升级中遇到的问题,主要是 TS 的类型问题。

forwardRef

终于可以用 props.ref 代替臭名昭著的 forwardRef,我认为这是最令人欣喜的新特性。被 forwardRef 折磨了这许久,终于 React 要干掉它了。

但对于 TS 代码来说,可能会遇到一些问题,比如我之前这么写(ref 没有写成可选参数):

function MyComp(props: MyCompProps, ref: Ref<MyCompRef>): ReactElement;

升级 19 后会报错签名不符,如下图:

这种情况,如果是 NPM 包,建议先发个兼容包,改 ref 为可选即可(然后再发最低依赖为 19 的大版本包):

function MyComp(props: MyCompProps, ref?: Ref<MyCompRef>): ReactElement;

useReducer

会写 TS 的人都知道,同一个方法,使用泛型的话,可以有多种不同的定义方式,好的定义能让开发者事半功倍。

useReducer 的类型定义就变了,其实升级文档里有提到,但心急的开发者估计会看漏,Better useReducer typings,而且 Codemod 工具并不会修正这里的问题。

所以对于类型定义完整的 TS 项目,会有冲击,导致构建失败。

// React 18 的 `useReducer` 5 个重载
function useReducer<R extends ReducerWithoutAction<any>, I>(
    reducer: R,
    initializerArg: I,
    initializer: (arg: I) => ReducerStateWithoutAction<R>
): [ReducerStateWithoutAction<R>, DispatchWithoutAction];

function useReducer<R extends ReducerWithoutAction<any>>(
    reducer: R,
    initializerArg: ReducerStateWithoutAction<R>,
    initializer?: undefined
): [ReducerStateWithoutAction<R>, DispatchWithoutAction];

function useReducer<R extends Reducer<any, any>, I>(
    reducer: R,
    initializerArg: I & ReducerState<R>,
    initializer: (arg: I & ReducerState<R>) => ReducerState<R>
): [ReducerState<R>, Dispatch<ReducerAction<R>>];

function useReducer<R extends Reducer<any, any>, I>(
    reducer: R,
    initializerArg: I,
    initializer: (arg: I) => ReducerState<R>
): [ReducerState<R>, Dispatch<ReducerAction<R>>];

function useReducer<R extends Reducer<any, any>>(
    reducer: R,
    initialState: ReducerState<R>,
    initializer?: undefined
): [ReducerState<R>, Dispatch<ReducerAction<R>>];

// React 19 的 `useReducer` 只剩下了两种
function useReducer<S, A extends AnyActionArg>(
    reducer: (prevState: S, ...args: A) => S,
    initialState: S
): [S, ActionDispatch<A>];

function useReducer<S, I, A extends AnyActionArg>(
    reducer: (prevState: S, ...args: A) => S,
    initialArg: I,
    init: (i: I) => S
): [S, ActionDispatch<A>];

主要区别是,19 把之前的 R(Reducer)拆成了 S(State) 和 A(Action)。

于是之前能跑通的构建失败了:

这种情况就得自己改了:

-const [state, dispatch] = useReducer<TModelReducer, null>(reducer, null, createInitialState);
+const [state, dispatch] = useReducer<IModelState, null, [TModelAction]>(reducer, null, createInitialState);

注意,泛型第三个参数必须是元组,需要中括号括起来(个人认为他们可以优化更彻底些,不需要是元组)。

useRef

useRef 类型定义也变了,参数变成了必填,也会导致构建失败:

// React 18
function useRef<T>(initialValue: T): MutableRefObject<T>;
function useRef<T>(initialValue: T | null): RefObject<T>;
function useRef<T = undefined>(initialValue?: undefined): MutableRefObject<T | undefined>;
// React 19
function useRef<T>(initialValue: T): RefObject<T>;  
function useRef<T>(initialValue: T | null): RefObject<T | null>;  
function useRef<T>(initialValue: T | undefined): RefObject<T | undefined>;

这意味着,需要将所有的空 useRef<T>() 加上默认值,哪怕传的是 undefined 也要写一下。

不过有一个好处,就是以前写成 useRef<T | null>(null) 的,现在只需要写 useRef<T>(null),见下图 React 18 和 19 下 IDE 类型推导的区别:

以前的 RefObject<T> 其实是现在的 RefObject<T | null>,现在的 RefObject<T> 是真的 RefObject<T>

Ref 的各种类型

上面的 useRef 类型定义,你可能已经注意到 MutableRefObject 的地方都被换成了 RefObject。以下是两个版本跟 Ref 有关的类型定义剪影:

React 18:

interface RefObject<T> {
  readonly current: T | null;
}
interface MutableRefObject<T> {
  current: T;
}
type Ref<T> = RefCallback<T> | RefObject<T> | null;
type ForwardedRef<T> = ((instance: T | null) => void) | MutableRefObject<T | null> | null;

React 19:

interface RefObject<T> {
  current: T; // 🎉 去掉 readonly
}
interface MutableRefObject<T> { // 💥 deprecated
  current: T;
}
type Ref<T> = RefCallback<T> | RefObject<T | null> | null;
type ForwardedRef<T> = ((instance: T | null) => void) | RefObject<T | null> | null;

变化:

  1. RefObject 不再只读(即不再需要 MutableRefObject),可以全局替换 MutableRefObjectRefObject
  2. ForwardedRef 虽然未标记 deprecated,但从其实现来看,可以全局替换 ForwardedRefRef

终于不再纠结那么多的 Ref 类型了。

☄️ 从 useImperativeHandle 再看 forwardRef

useImperativeHandle 是一个可能相对比较冷门的 Hook,但它真的很管用,可以让父组件调用子组件提供的方法,从而避免非常不 React 的写法(比如使用事件通知这种耦合性、不确定性比较强的写法)。

当一个组件复杂到一定程度的时候,为了避免 Props Drilling 的问题,我通常会用 Context 作为组件内全局数据状态管理的工具。所有与 propsstateeffect 相关的逻辑通通在一个不涉及 UI 的「Model」层进行封装,UI 与 Model 之间惟一的桥梁就是 Hooks。

React ≤ 18 的绕脑写法

React 19 之前,这种模式在使用 useImperativeHandle 的时候会写出很绕的代码,为了 ref 能够最终有效,我需要至少写两次 forwardRef,而且比较晦涩,这让我苦不堪言(所以我之前对于写 useImperativeHandle 多少是有些惧怕的)。

React 18 及之前,可能需要这么写。

组件 Model + UI:

import {
  ReactElement,
  ForwardedRef,
  forwardRef
} from 'react';

import Model, {
  ModelProps,
  ImperativeRef
} from '../model';
import Ui from '../ui';
  
export default forwardRef(function TheComponent(props: ModelProps, ref: ForwardedRef<ImperativeRef>): ReactElement {
  return <Model {...props}>
    <Ui ref={ref} />
  </Model>;
});

UI 层:

import {
  ReactElement,
  ForwardedRef,
  useImperativeHandle,
  forwardRef
} from 'react';

import {  
  ImperativeRef,
  useRefImperative
} from '../model';

export default forwardRef(function Ui(_props: unknown, ref: ForwardedRef<ImperativeRef>): ReactElement {
  const imperativeRef = useRefImperative();
  
  useImperativeHandle(ref, () => imperativeRef, [imperativeRef]);
  
  return <... />;
});

很绕,是不是?需要在组件最外层,把 ref 传递到 UI 层,然后再在 UI 层 useImperativeHandle,为此,原本可以无参的 UI 组件,甚至还要写个 _props: unknown

但凡脑子不好一点都想不出这么绕的法子 😳。但为了能够在正确的位置使用 useImperativeHandle,这的确是我能想到的比较「高明」的办法了。

React 19 的写法

React 19 变得相当简单,forwardRef 全部干掉后,改造 Model 内部的 useRefImperative,使 Model 更内聚:

import {  
  useImperativeHandle  
} from 'react';  
  
import {  
  IImperativeRef  
} from '../types';  
  
import useModelProps from './_use-model-props';  
  
export default function useRefImperative(): void {  
  const {  
    ref  
  } = useModelProps();  
    
  useImperativeHandle(ref, (): IImperativeRef => ({  
    ...  
  }), [...]);  
}

使用的话,就只需要在 UI 组件,简单调用一下 useRefImperative 即可:

import {
  ReactElement
} from 'react';

import {  
  useRefImperative
} from '../model';

export default function Ui(): ReactElement {
  useRefImperative();
  
  return <... />;
});

至此,我们再也不需要惧怕写 useImperativeHandle 了。

🐌 组件库怎么办

React 目前尚未对 forwardRef 标记 deprecated,但也说不久的将来会这么做。

组件库就会比较尴尬,考虑到存量应用,组件库没法在升级 React 19 后直接废弃 forwardRef。也就是说,组件库一时间还没办法享受弃用 forwardRef 的带来的红利,除非组件库启用大版本不兼容升级。

拿 Antd 举例,Antd 5 支持的最小 React 版本是 16.9.0,Antd 6 却并没有直接提升到 React 19,而仅仅声明最小 React 版本是 18。所以,它的实现依然使用了 forwardRef(而且只能用 forwardRef)。

🪭 总结

这次的升级,BREAK CHANGE 不多,总的来说比较平滑顺畅,我用了 1 天的时间升级完了五个项目,总结下来就这些:

  1. 所有的 forwardRef 可以改成 props.ref(如果是 NPM 包,需要先兼容,后发大版本)
  2. useReducer 的类型为 BREAK CHANGE,但也只需要改类型,修改相对简单
  3. useRef<T>() 传空将导致构建失败,可改成 useRef<T>(null)useRef<T>(undefined)
  4. MutableRefObjectRefObject
  5. ForwardedRefRef
  6. Context.ProviderContext
  7. 组件库,除非声明支持最小 React 版本为 19,不要杀 fowardRef(也不要用 props.ref

React - 【useEffect 与 useLayoutEffect】 区别 及 使用场景

2025年11月24日 20:27

useEffect vs useLayoutEffect 完全指南

🎯 核心区别:执行时机

useEffect(异步副作用)

React 渲染 DOM
    ↓
浏览器绘制屏幕
    ↓
执行 useEffect ← 用户已经看到 DOM
    ↓
可能导致屏幕闪烁

useLayoutEffect(同步副作用)

React 渲染 DOM
    ↓
执行 useLayoutEffect ← 立即同步执行
    ↓
浏览器绘制屏幕(已是最终效果)
    ↓
用户看到最终结果(无闪烁)

📊 详细对比表

特性 useEffect useLayoutEffect
执行时机 DOM 更新后,浏览器绘制前 DOM 更新后,立即同步执行
阻塞渲染 ❌ 不阻塞 ✅ 会阻塞浏览器绘制
性能 ⭐⭐⭐⭐⭐ 更好 ⭐⭐⭐ 可能卡顿
使用频率 99% 的情况用这个 特定场景用
服务端渲染 ✅ 支持 ❌ 会报警告
清理函数执行 DOM 更新前执行 DOM 更新前执行

🔴 useEffect 执行时间线

function Component() {
  useEffect(() => {
    console.log('1. useEffect 执行');
    return () => console.log('2. useEffect 清理函数');
  }, []);

  console.log('0. 组件渲染');
  return <div>Hello</div>;
}

// 执行顺序:
// 0. 组件渲染
// 1. useEffect 执行
// 2. useEffect 清理函数(卸载或依赖变化时)

🟠 useLayoutEffect 执行时间线

function Component() {
  useLayoutEffect(() => {
    console.log('1. useLayoutEffect 执行(同步)');
    return () => console.log('2. useLayoutEffect 清理函数');
  }, []);

  console.log('0. 组件渲染');
  return <div>Hello</div>;
}

// 执行顺序:
// 0. 组件渲染
// 1. useLayoutEffect 执行(同步,阻塞浏览器绘制)
// 浏览器绘制屏幕
// 2. useLayoutEffect 清理函数

💡 常用场景 & 代码示例

场景1️⃣:数据请求(useEffect)✅ 推荐

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    setLoading(true);
    
    // 异步获取数据
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        setUser(data);
        setLoading(false);
      })
      .catch(err => {
        console.error('加载失败:', err);
        setLoading(false);
      });
  }, [userId]);  // userId 变化时重新获取

  if (loading) return <div>加载中...</div>;
  return <div>{user?.name}</div>;
}

为什么用 useEffect?

  • 数据获取是异步操作,不需要同步阻塞
  • 用户看到加载状态很正常

场景2️⃣:事件监听(useEffect)✅ 推荐

function WindowResize() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    function handleResize() {
      setWidth(window.innerWidth);
    }

    // 添加监听
    window.addEventListener('resize', handleResize);

    // 清理:移除监听
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);  // 只在挂载时运行

  return <div>窗口宽度: {width}px</div>;
}

为什么用 useEffect?

  • 事件监听是异步的,不需要同步执行
  • 必须清理事件监听器,防止内存泄漏

场景3️⃣:DOM 测量(useLayoutEffect)✅ 正确

function MeasureElement() {
  const ref = useRef(null);
  const [height, setHeight] = useState(0);

  useLayoutEffect(() => {
    // 必须用 useLayoutEffect,否则会闪烁
    // 1. 测量 DOM 尺寸
    const rect = ref.current.getBoundingClientRect();
    
    // 2. 基于尺寸计算布局
    const newHeight = rect.width > 500 ? 300 : 150;
    
    // 3. 立即更新(在浏览器绘制前)
    setHeight(newHeight);
  }, []);  // 只在挂载时运行

  return (
    <div ref={ref} style={{ height: `${height}px` }}>
      响应式高度
    </div>
  );
}

为什么用 useLayoutEffect?

  • 需要在浏览器绘制前完成 DOM 测量和布局计算
  • 如果用 useEffect,用户会看到高度从 0 闪到 300(闪烁!)

场景4️⃣:表单焦点设置(useLayoutEffect)✅ 正确

function AutoFocusForm() {
  const inputRef = useRef(null);

  useLayoutEffect(() => {
    // 必须用 useLayoutEffect,否则表单打开时看不到光标在哪
    inputRef.current?.focus();
  }, []);

  return (
    <form>
      <input ref={inputRef} placeholder="自动聚焦" />
      <button type="submit">提交</button>
    </form>
  );
}

为什么用 useLayoutEffect?

  • 如果用 useEffect,用户会看到输入框出现,然后光标才聚焦(视觉延迟)
  • useLayoutEffect 保证光标在浏览器绘制前已经聚焦

场景5️⃣:动画初始化(useLayoutEffect)✅ 正确

function AnimatedBox() {
  const boxRef = useRef(null);

  useLayoutEffect(() => {
    // 设置初始状态(隐藏)
    gsap.set(boxRef.current, { opacity: 0, x: -50 });
    
    // 然后执行动画
    gsap.to(boxRef.current, { 
      opacity: 1, 
      x: 0, 
      duration: 0.5 
    });
  }, []);

  return <div ref={boxRef}>动画盒子</div>;
}

为什么用 useLayoutEffect?

  • 避免用户看到初始未变换的状态,然后才动画
  • useLayoutEffect 保证在浏览器绘制前完成初始设置

场景6️⃣:主题切换(useLayoutEffect)⚠️ 特殊

function ThemeProvider({ theme }) {
  useLayoutEffect(() => {
    // 立即应用主题,避免闪烁
    document.documentElement.style.colorScheme = theme;
    document.body.className = `theme-${theme}`;
  }, [theme]);

  return <div>内容</div>;
}

为什么用 useLayoutEffect?

  • 防止主题切换时的闪烁(深色→浅色 的白屏闪烁)

📋 依赖数组详解

// 1️⃣ 每次渲染都执行(没有依赖数组)
useEffect(() => {
  console.log('每次都运行');
});

// 2️⃣ 只在挂载时执行(空依赖数组)
useEffect(() => {
  console.log('只在挂载时执行');
}, []);

// 3️⃣ 依赖值变化时执行
useEffect(() => {
  console.log('userId 变化时执行');
}, [userId]);

// 4️⃣ 多个依赖
useEffect(() => {
  console.log('userId 或 pageNum 变化时执行');
}, [userId, pageNum]);

// 5️⃣ 依赖是对象时的坑
useEffect(() => {
  // ❌ 每次都会执行,因为 {} 每次都是新的
  console.log('可能无限循环');
}, [{}]);

// ✅ 正确做法:使用 useMemo
const config = useMemo(() => ({}), []);
useEffect(() => {
  console.log('config 稳定,不会无限循环');
}, [config]);

⚠️ 常见陷阱

陷阱1️⃣:useLayoutEffect 导致卡顿

// ❌ 不好:useLayoutEffect 中做复杂计算会阻塞绘制
useLayoutEffect(() => {
  // 这会冻结浏览器!
  for (let i = 0; i < 1000000; i++) {
    complexCalculation();
  }
}, []);

// ✅ 好:复杂计算放到 useEffect
useEffect(() => {
  for (let i = 0; i < 1000000; i++) {
    complexCalculation();
  }
}, []);

陷阱2️⃣:useLayoutEffect 在服务端渲染中报错

// ❌ 错:SSR 时会报警告
useLayoutEffect(() => {
  // SSR 中不能执行
}, []);

// ✅ 好:检测环境
useEffect(() => {
  if (typeof window !== 'undefined') {
    // 客户端代码
  }
}, []);

// 或者条件性使用
const useIsomorphicLayoutEffect = 
  typeof window !== 'undefined' ? useLayoutEffect : useEffect;

useIsomorphicLayoutEffect(() => {
  // 自动适配 SSR
}, []);

陷阱3️⃣:忘记清理副作用

// ❌ 坏:内存泄漏
useEffect(() => {
  const timer = setInterval(() => {
    console.log('每秒执行');
  }, 1000);
  // 没有清理!
}, []);

// ✅ 好:返回清理函数
useEffect(() => {
  const timer = setInterval(() => {
    console.log('每秒执行');
  }, 1000);
  
  return () => clearInterval(timer);  // 清理定时器
}, []);

陷阱4️⃣:无限循环

// ❌ 坏:无限循环
useEffect(() => {
  setCount(count + 1);  // 每次都修改 count
}, [count]);  // 导致重新渲染,又触发 effect

// ✅ 好:不把更新的值放在依赖中
useEffect(() => {
  setCount(c => c + 1);
}, []);  // 空依赖,只执行一次

🚀 选择流程图

需要执行副作用?
    ↓
是异步操作(数据获取、定时器)?
    ├─ 是 → useEffect ✅
    └─ 否 ↓
    
需要在浏览器绘制前同步执行?
    ├─ 是 → useLayoutEffect ✅
    │   例:DOM 测量、焦点、动画、主题
    └─ 否 → useEffect ✅(默认选择)

📊 性能对比

// 测试性能影响

function PerformanceTest() {
  const [count, setCount] = useState(0);

  // useEffect 不会阻塞渲染
  useEffect(() => {
    // 即使这里做复杂计算,页面也能快速响应
    for (let i = 0; i < 10000000; i++) {}
  }, [count]);

  // useLayoutEffect 会阻塞渲染
  useLayoutEffect(() => {
    // 这个计算会让点击按钮时出现明显延迟
    for (let i = 0; i < 10000000; i++) {}
  }, [count]);

  return (
    <button onClick={() => setCount(count + 1)}>
      点击:{count}
    </button>
  );
}

// 结论:
// useEffect 按钮响应迅速
// useLayoutEffect 点击时会卡顿

✅ 快速参考

99% 情况用 useEffect

// 数据获取
useEffect(() => { fetchData(); }, []);

// 事件监听
useEffect(() => { 
  window.addEventListener('resize', handler);
  return () => window.removeEventListener('resize', handler);
}, []);

// 副作用通用场景
useEffect(() => { doSomething(); }, [dep]);

特殊场景用 useLayoutEffect

// DOM 测量
useLayoutEffect(() => { 
  const rect = ref.current.getBoundingClientRect();
  setHeight(rect.height);
}, []);

// 焦点设置
useLayoutEffect(() => { 
  inputRef.current?.focus();
}, []);

// 动画初始化
useLayoutEffect(() => { 
  gsap.set(ref.current, initialState);
  startAnimation();
}, []);

🎓 总结

维度 useEffect useLayoutEffect
何时用 99% 的情况 需要同步 DOM 操作
执行时机 浏览器绘制后 浏览器绘制前
是否阻塞 不阻塞 会阻塞
性能 优秀 可能卡顿
例子 数据请求、事件监听 DOM 测量、焦点、动画

记住:优先用 useEffect,只有特殊场景才用 useLayoutEffect! 🎯

Qt 6 实战:C++ 调用 QML 回调方法(异步场景完整实现)

作者 喵个咪
2025年11月24日 19:58

Qt 6 实战:C++ 调用 QML 回调方法(异步场景完整实现)

在 Qt 6 开发中,C++ 与 QML 混合编程是常见场景。当 C++ 处理异步操作(如登录验证、网络请求、数据库查询)时,需要将结果通知给 QML 界面,回调函数是最直观的通信方式之一。本文将基于你提供的代码框架,补充关键细节、修复潜在问题,并完整实现从 C++ 调用 QML 回调的全流程。

一、核心场景说明

我们需要实现:

  1. QML 调用 C++ 的 login 方法(传入用户名、密码和两个回调函数:成功回调 onSuccess、失败回调 onFailure);
  2. C++ 异步处理登录逻辑(模拟耗时操作);
  3. 登录完成后,C++ 调用对应的 QML 回调函数,将结果(成功响应 / 错误信息)传递给 QML。

二、Step 1:完善 C++ 服务类

1.1 基础配置(必须继承 QObject)

QML 能调用的 C++ 方法 / 属性,依赖 Qt 的元对象系统(MOC),因此 AuthenticationService 必须:

  • 继承 QObject
  • 添加 Q_OBJECT 宏;
  • Q_INVOKABLE 标记需要暴露给 QML 的方法。

1.2 完整 C++ 代码实现

// authentication_service.h
#include <QObject>
#include <QJSValue>
#include <QJSEngine>
#include <QtConcurrent>
#include <QThread>
#include <QMetaObject>

// 自定义错误类:封装错误码、错误信息
class KratosError {
public:
    KratosError(int code, const QString& message, const QString& details = "")
        : m_code(code), m_message(message), m_details(details) {}

    // 转换为 QJSValue,供 QML 访问属性
    QJSValue toQJSValue(QJSEngine& engine) const {
        QJSValue errorObj = engine.newObject();
        errorObj.setProperty("code", m_code);       // 错误码(如 401 未授权)
        errorObj.setProperty("message", m_message); // 错误提示
        errorObj.setProperty("details", m_details); // 详细信息(可选)
        return errorObj;
    }

private:
    int m_code;
    QString m_message;
    QString m_details;
};

// 登录成功响应类:封装返回数据
struct LoginResponse {
    QString token;     // 身份令牌
    QString username;  // 用户名
    int userId;        // 用户 ID

    // 转换为 QJSValue,供 QML 访问属性
    QJSValue toQJSValue(QJSEngine& engine) const {
        QJSValue respObj = engine.newObject();
        respObj.setProperty("token", token);
        respObj.setProperty("username", username);
        respObj.setProperty("userId", userId);
        return respObj;
    }
};

// 认证服务类(单例模式)
class AuthenticationService : public QObject {
    Q_OBJECT // 必须添加,启用元对象系统
public:
    // 单例获取方法(线程安全)
    static AuthenticationService* instance() {
        static QMutex mutex;
        if (!m_instance) {
            mutex.lock();
            if (!m_instance) {
                m_instance = new AuthenticationService();
            }
            mutex.unlock();
        }
        return m_instance;
    }

    // 禁止拷贝构造和赋值
    AuthenticationService(const AuthenticationService&) = delete;
    AuthenticationService& operator=(const AuthenticationService&) = delete;

    // 暴露给 QML 的登录方法
    Q_INVOKABLE void login(
        const QString& username,
        const QString& password,
        const QJSValue& onSuccess,  // QML 传入的成功回调
        const QJSValue& onFailure   // QML 传入的失败回调
    );

private:
    AuthenticationService(QObject* parent = nullptr) : QObject(parent) {}
    static AuthenticationService* m_instance;
};

// authentication_service.cpp
#include "authentication_service.h"

AuthenticationService* AuthenticationService::m_instance = nullptr;

void AuthenticationService::login(
    const QString& username,
    const QString& password,
    const QJSValue& onSuccess,
    const QJSValue& onFailure
) {
    // 1. 有效性检查:确保 QJSEngine 和回调函数有效
    QJSEngine* engine = qjsEngine(this);
    if (!engine) {
        qWarning() << "[AuthService] 失败:无法获取 QJSEngine 上下文";
        return;
    }
    if (!onSuccess.isCallable() && !onFailure.isCallable()) {
        qWarning() << "[AuthService] 警告:未传入有效回调函数";
        return;
    }

    // 2. 异步处理登录逻辑(模拟耗时操作:如网络请求、数据库验证)
    // 用 QtConcurrent::run 开启后台线程,避免阻塞 UI 线程
    QtConcurrent::run([=, this]() {
        // 模拟耗时 2 秒(实际场景替换为真实登录逻辑)
        QThread::sleep(2);

        // 3. 模拟登录验证结果(实际场景替换为真实校验逻辑)
        bool isLoginSuccess = (username == "admin" && password == "123456");

        // 4. 切换回主线程执行回调(关键!QJSValue 必须在创建它的线程调用)
        QMetaObject::invokeMethod(this, [=, this]() {
            if (isLoginSuccess) {
                // 登录成功:构造响应对象,调用 onSuccess 回调
                if (onSuccess.isCallable()) {
                    LoginResponse resp;
                    resp.token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
                    resp.username = username;
                    resp.userId = 1001;

                    QJSValueList args;
                    args << resp.toQJSValue(*engine); // 传入响应数据
                    onSuccess.call(args);             // 调用 QML 成功回调
                }
            } else {
                // 登录失败:构造错误对象,调用 onFailure 回调
                if (onFailure.isCallable()) {
                    KratosError error(401, "登录失败", "用户名或密码错误");

                    QJSValueList args;
                    args << error.toQJSValue(*engine); // 传入错误信息
                    onFailure.call(args);              // 调用 QML 失败回调
                }
            }
        }, Qt::QueuedConnection); // 队列连接:确保在主线程执行
    });
}

三、Step 2:注册 C++ 单例到 QML

main.cpp 中,将 AuthenticationService 单例注册到 QML 上下文,让 QML 可以直接访问:

// main.cpp
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include "authentication_service.h"

int main(int argc, char *argv[]) {
    QGuiApplication app(argc, argv);

    QQmlApplicationEngine engine;

    // 注册 C++ 单例到 QML(模块名:backend,版本:1.0,对象名:AuthenticationService)
    qmlRegisterSingletonInstance(
        "backend",                // QML 导入时的模块名
        1, 0,                     // 版本号(需与 QML import 一致)
        "AuthenticationService",  // QML 中访问的对象名
        AuthenticationService::instance() // 单例实例
    );

    // 加载 QML 主文件
    const QUrl url(u"qrc:/LoginDemo/main.qml"_qs);
    QObject::connect(&engine, &QQmlApplicationEngine::objectCreationFailed,
        &app, []() { QCoreApplication::exit(-1); },
        Qt::QueuedConnection);
    engine.load(url);

    return app.exec();
}

关键注意:

  • 注册时模块名(backend)、版本号(1.0)必须与 QML 中的 import 语句一致;
  • 单例注册需用 qmlRegisterSingletonInstance(Qt 5.15+ 支持,Qt 6 推荐),而非 qmlRegisterSingletonType(后者适合动态创建单例)。

四、Step 3:QML 中调用 C++ 方法并处理回调

在 QML 界面中,导入注册的模块,调用 AuthenticationService.login 并传入回调函数:

// main.qml
import QtQuick 6.2
import QtQuick.Controls 6.2
import backend 1.0 // 导入 C++ 注册的模块(需与注册时的模块名、版本一致)

ApplicationWindow {
    width: 400
    height: 300
    title: "登录演示"
    visible: true

    ColumnLayout {
        anchors.centerIn: parent
        spacing: 16

        TextField {
            id: usernameField
            placeholderText: "输入用户名"
            Layout.width: 250
            text: "admin" // 测试用默认值
        }

        TextField {
            id: passwordField
            placeholderText: "输入密码"
            echoMode: TextField.Password
            Layout.width: 250
            text: "123456" // 测试用默认值(正确密码)
            // text: "wrong" // 测试失败场景
        }

        Button {
            text: "登录"
            Layout.width: 250
            onClicked: {
                // 调用 C++ 的 login 方法,传入回调函数
                AuthenticationService.login(
                    usernameField.text,
                    passwordField.text,
                    // 成功回调:接收 C++ 传递的响应数据
                    function(response) {
                        console.log("登录成功!响应:", JSON.stringify(response))
                        // 访问响应属性(C++ 中 LoginResponse 的字段)
                        console.log("Token:", response.token)
                        console.log("用户名:", response.username)
                    },
                    // 失败回调:接收 C++ 传递的错误信息
                    function(error) {
                        console.log("登录失败!错误码:", error.code, " 信息:", error.message)
                        // 在界面显示错误提示
                        errorLabel.text = error.message
                    }
                )
            }
        }

        Label {
            id: errorLabel
            color: "red"
            Layout.width: 250
            horizontalAlignment: Text.AlignCenter
        }
    }
}

五、核心技术关键点解析

1. QJSValue:C++ 与 QML 回调的桥梁

  • QJSValue 是 Qt 中封装 JavaScript 值的类,支持存储函数、对象、基本类型等;
  • isCallable() 检查是否为可调用的 JavaScript 函数(回调);
  • call(QJSValueList args) 调用回调函数,参数通过 QJSValueList 传递。

2. 线程安全(重中之重)

  • QML 的 QJSEngine线程关联的(默认绑定主线程),直接在后台线程调用 QJSValue::call 会导致崩溃;
  • 解决方案:用 QMetaObject::invokeMethod + Qt::QueuedConnection,将回调调用切换到主线程执行。

3. 自定义数据类型转 QJSValue

  • 自定义类(如 KratosErrorLoginResponse)需提供 toQJSValue 方法,通过 QJSEngine::newObject() 创建 JS 对象,再用 setProperty 设置属性;
  • QML 中可直接通过属性名访问(如 error.messageresponse.token),大小写敏感。

4. 有效性检查

  • 必须检查 QJSEngine* engine = qjsEngine(this) 是否为空(避免 QML 组件销毁后引擎失效);
  • 必须检查回调函数 isCallable()(避免传入非函数类型导致崩溃)。

六、常见问题排查

1. QML 无法导入 backend 模块?

  • 检查 qmlRegisterSingletonInstance 的模块名、版本号与 QML import 一致;
  • 确保 C++ 类继承 QObject 并添加 Q_OBJECT 宏;
  • 构建时确保 MOC 文件正常生成(qmake 自动处理,CMake 需添加 qt_add_qml_module)。

2. 回调函数不执行?

  • 检查 isCallable() 是否返回 true(确认传入的是函数);
  • 检查是否在主线程调用 call()(后台线程调用会静默失败);
  • 检查异步逻辑是否正常执行(如模拟的 QThread::sleep 后是否触发回调)。

3. 程序崩溃?

  • 大概率是线程问题:后台线程直接操作 QJSValueQJSEngine
  • 检查 engine 是否为空(如单例销毁后仍调用回调)。

七、最佳实践

1. 优先使用回调还是信号槽?

  • 回调:适合一次性操作(如登录、单次网络请求),代码直观,参数传递灵活;
  • 信号槽:适合多次通知(如实时数据更新),解耦性更强,支持多订阅者;
  • 本文场景(登录)用回调更合适,简洁高效。

2. 简化数据传递(可选)

若数据简单,可直接用 QVariantMap 代替自定义类,无需写 toQJSValue 方法:

QVariantMap errorMap;
errorMap["code"] = 401;
errorMap["message"] = "登录失败";
onFailure.call(QJSValueList{engine->toScriptValue(errorMap)});

3. 避免回调地狱

若存在多层回调(如登录后调用获取用户信息),可考虑用 Qt 6 的 QPromise + co_await(C++20+)或 QML 的 async/await 优化。

八、总结

本文完整实现了 Qt 6 中 C++ 调用 QML 回调的流程,核心是:

  1. C++ 类继承 QObject 并暴露 Q_INVOKABLE 方法;
  2. QJSValue 接收 QML 回调,用 call() 触发回调;
  3. 异步场景下必须切换到主线程执行回调,确保线程安全;
  4. 自定义数据通过 QJSValue 转换后传递,QML 可直接访问属性。

这种方式适用于所有异步通信场景(登录、网络请求、文件读写等),是 C++ 与 QML 协作的核心技巧之一。

kotlin-6

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

太棒了!现在让我们探索 Kotlin 的终极境界——元编程、编译器插件、深度优化未来特性。这将带你进入 Kotlin 语言设计的核心领域。


Kotlin 终极探索:元编程与语言深度

三十九、注解处理器与代码生成(KSP)

1. 使用 KSP(Kotlin Symbol Processing)
// 1. 定义注解
@Target(AnnotationTarget.CLASS)
annotation class JsonSerializable

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

@Target(AnnotationTarget.PROPERTY)
annotation class JsonExclude

// 2. 定义数据类(这些将在编译时被处理)
@JsonSerializable
data class Person(
    @JsonName("person_name")
    val name: String,
    val age: Int,
    @JsonExclude
    val password: String
)

// 3. 手动模拟 KSP 处理结果(实际由注解处理器生成)
class PersonSerializer {
    fun toJson(person: Person): String {
        return buildString {
            append("{")
            append(""person_name": "${person.name}", ")
            append(""age": ${person.age}")
            append("}")
        }
    }
    
    companion object {
        fun fromJson(json: String): Person {
            // 简化的 JSON 解析逻辑
            val name = json.substringAfter(""person_name": "").substringBefore(""")
            val age = json.substringAfter(""age": ").substringBefore("}").toInt()
            return Person(name, age, "")
        }
    }
}

// 4. 使用生成的序列化器
fun testManualKSP() {
    val person = Person("Alice", 25, "secret")
    val serializer = PersonSerializer()
    val json = serializer.toJson(person)
    println("生成的 JSON: $json")
    
    val parsedPerson = PersonSerializer.fromJson(json)
    println("解析的对象: $parsedPerson")
}
2. 构建时元编程模式
// 编译时验证注解
@Target(AnnotationTarget.CLASS)
annotation class Validate(val fields: Array<String>)

@Validate(fields = ["age", "email"])
data class User(val name: String, val age: Int, val email: String)

// 运行时验证逻辑(模拟编译时生成)
class UserValidator {
    companion object {
        fun validate(user: User): List<String> {
            val errors = mutableListOf<String>()
            
            if (user.age < 0) errors.add("年龄不能为负数")
            if (!user.email.contains("@")) errors.add("邮箱格式不正确")
            
            return errors
        }
    }
}

// 使用编译时验证
fun testValidation() {
    val user = User("Bob", -5, "invalid-email")
    val errors = UserValidator.validate(user)
    
    if (errors.isNotEmpty()) {
        println("验证错误: ${errors.joinToString()}")
    } else {
        println("验证通过")
    }
}

四十、编译器插件开发基础

1. 自定义编译器扩展模式
// 1. 定义领域特定语言
class SqlBuilder {
    private var table: String = ""
    private val conditions = mutableListOf<String>()
    private val joins = mutableListOf<String>()
    private var limit: Int? = null
    
    infix fun String.eq(value: Any) = "$this = '$value'"
    infix fun String.`in`(values: List<Any>) = 
        "$this IN (${values.joinToString { "'$it'" }})"
    
    fun from(tableName: String) {
        table = tableName
    }
    
    fun where(condition: String) {
        conditions.add(condition)
    }
    
    fun join(table: String, on: String) {
        joins.add("JOIN $table ON $on")
    }
    
    fun limit(count: Int) {
        limit = count
    }
    
    fun build(): String {
        val query = StringBuilder("SELECT * FROM $table")
        
        if (joins.isNotEmpty()) {
            query.append(" ").append(joins.joinToString(" "))
        }
        
        if (conditions.isNotEmpty()) {
            query.append(" WHERE ").append(conditions.joinToString(" AND "))
        }
        
        limit?.let {
            query.append(" LIMIT $it")
        }
        
        return query.toString()
    }
}

// 2. 使用 DSL 构建 SQL
fun testSqlBuilder() {
    val query = SqlBuilder().apply {
        from("users")
        where("name" eq "Alice")
        where("age" `in` listOf(25, 30, 35))
        join("orders", "users.id = orders.user_id")
        limit(10)
    }.build()
    
    println("生成的 SQL: $query")
}
2. 类型安全构建器进阶
// 类型安全的 HTTP 请求构建器
class HttpRequestBuilder {
    var method: String = "GET"
    var url: String = ""
    val headers = mutableMapOf<String, String>()
    var body: String = ""
    
    fun method(method: String) {
        this.method = method
    }
    
    fun url(url: String) {
        this.url = url
    }
    
    fun header(name: String, value: String) {
        headers[name] = value
    }
    
    fun body(content: String) {
        this.body = content
        header("Content-Length", content.length.toString())
    }
    
    fun execute(): String {
        // 模拟 HTTP 请求
        return "HTTP $method $url - Headers: $headers - Body: $body"
    }
}

// DSL 扩展函数
fun httpRequest(block: HttpRequestBuilder.() -> Unit): String {
    val builder = HttpRequestBuilder()
    builder.block()
    return builder.execute()
}

// 使用类型安全构建器
fun testHttpBuilder() {
    val response = httpRequest {
        method = "POST"
        url = "https://api.example.com/users"
        header("Authorization", "Bearer token123")
        header("Content-Type", "application/json")
        body = """{"name": "Alice", "age": 25}"""
    }
    
    println("HTTP 响应: $response")
}

四十一、深度性能优化技术

1. 内联类与值类的性能优化
@JvmInline
value class Meter(val value: Double) {
    operator fun plus(other: Meter): Meter = Meter(value + other.value)
    operator fun times(factor: Double): Meter = Meter(value * factor)
    
    fun toKilometers(): Kilometer = Kilometer(value / 1000.0)
}

@JvmInline
value class Kilometer(val value: Double) {
    fun toMeters(): Meter = Meter(value * 1000.0)
}

// 使用内联类进行类型安全计算
fun calculateDistance() {
    val distance1 = Meter(500.0)
    val distance2 = Meter(300.0)
    val total = distance1 + distance2
    val scaled = total * 2.5
    
    println("总距离: ${total.value} 米")
    println("缩放后: ${scaled.value} 米")
    println("转换为公里: ${scaled.toKilometers().value} 公里")
    
    // 编译错误:类型安全
    // val invalid = distance1 + scaled.toKilometers() // 编译错误!
}

// 内联类的集合操作优化
@JvmInline
value class UserId(val value: Int)

fun optimizeCollections() {
    val userIds = listOf(UserId(1), UserId(2), UserId(3))
    
    // 这些操作在运行时不会产生包装对象开销
    val mapped = userIds.map { it.value * 2 }
    val filtered = userIds.filter { it.value > 1 }
    
    println("映射结果: $mapped")
    println("过滤结果: $filtered")
}
2. 内联函数与 reified 类型参数的高级用法
import kotlin.reflect.KClass

// 高级 reified 用法
inline fun <reified T : Any> createInstance(vararg args: Any): T {
    return when (T::class) {
        String::class -> "" as T
        Int::class -> 0 as T
        List::class -> emptyList<Any>() as T
        Map::class -> emptyMap<Any, Any>() as T
        else -> throw IllegalArgumentException("不支持的类型")
    }
}

// 类型安全的验证框架
inline fun <reified T> validate(value: Any, validator: T.() -> Boolean): Boolean {
    return if (value is T) {
        value.validator()
    } else {
        false
    }
}

// 使用高级内联函数
fun testAdvancedInline() {
    val stringInstance: String = createInstance()
    val listInstance: List<Any> = createInstance()
    
    println("创建的实例: $stringInstance, $listInstance")
    
    // 类型安全验证
    val email = "test@example.com"
    val isValidEmail = validate(email) {
        contains("@") && length > 5
    }
    
    val number = 42
    val isValidNumber = validate(number) {
        this in 1..100
    }
    
    println("邮箱验证: $isValidEmail")
    println("数字验证: $isValidNumber")
}

四十二、Kotlin 多平台项目(KMP)高级特性

1. 共享业务逻辑与平台特定实现
// 通用业务模型
expect class DateTimeFormatter {
    fun format(timestamp: Long): String
    fun parse(dateString: String): Long
}

class EventLogger(private val formatter: DateTimeFormatter) {
    private val events = mutableListOf<String>()
    
    fun logEvent(event: String) {
        val timestamp = System.currentTimeMillis()
        val formattedTime = formatter.format(timestamp)
        events.add("[$formattedTime] $event")
    }
    
    fun getEvents(): List<String> = events.toList()
    
    fun clear() {
        events.clear()
    }
}

// Android 实现
actual class DateTimeFormatter actual constructor() {
    actual fun format(timestamp: Long): String {
        // 使用 Android 的 DateFormat
        return android.text.format.DateFormat.format("yyyy-MM-dd HH:mm:ss", timestamp).toString()
    }
    
    actual fun parse(dateString: String): Long {
        // Android 特定的解析逻辑
        return java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(dateString).time
    }
}

// iOS 实现  
actual class DateTimeFormatter actual constructor() {
    actual fun format(timestamp: Long): String {
        // 使用 iOS 的 DateFormatter
        val formatter = iosFoundation.NSDateFormatter()
        formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
        return formatter.stringFromDate(iosFoundation.NSDate(timestamp))
    }
    
    actual fun parse(dateString: String): Long {
        val formatter = iosFoundation.NSDateFormatter()
        formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
        return formatter.dateFromString(dateString)?.timeIntervalSince1970?.toLong() ?: 0L
    }
}
2. KMP 中的并发处理
expect class PlatformCoroutineDispatcher {
    fun dispatch(block: () -> Unit)
}

actual class PlatformCoroutineDispatcher actual constructor() {
    actual fun dispatch(block: () -> Unit) {
        // Android: Handler(Looper.getMainLooper()).post(block)
        // iOS: DispatchQueue.main.async { block() }
        block() // 简化实现
    }
}

class CrossPlatformService(private val dispatcher: PlatformCoroutineDispatcher) {
    suspend fun fetchData(): String = withContext(createDispatcherContext(dispatcher)) {
        delay(1000) // 模拟网络请求
        "跨平台数据"
    }
    
    private fun createDispatcherContext(dispatcher: PlatformCoroutineDispatcher) = 
        object : CoroutineDispatcher() {
            override fun dispatch(context: CoroutineContext, block: Runnable) {
                dispatcher.dispatch { block.run() }
            }
        }
}

四十三、Kotlin 未来特性探索

1. 上下文接收器(Context Receivers)原型
// 模拟上下文接收器功能
interface DatabaseContext {
    val connection: DatabaseConnection
}

interface LoggingContext {
    val logger: Logger
}

class DatabaseConnection {
    fun executeQuery(sql: String): String = "查询结果: $sql"
}

class Logger {
    fun info(message: String) = println("[INFO] $message")
}

// 使用上下文参数模拟上下文接收器
class UserService(
    private val dbContext: DatabaseContext,
    private val logContext: LoggingContext
) {
    fun getUser(id: Int): String {
        logContext.logger.info("获取用户 $id")
        return dbContext.connection.executeQuery("SELECT * FROM users WHERE id = $id")
    }
}

// 简化使用方式
fun testContextReceivers() {
    val dbContext = object : DatabaseContext {
        override val connection = DatabaseConnection()
    }
    
    val logContext = object : LoggingContext {
        override val logger = Logger()
    }
    
    val userService = UserService(dbContext, logContext)
    val result = userService.getUser(1)
    println(result)
}
2. 自定义操作符与 DSL 深度集成
// 自定义数学 DSL
class Vector(val x: Double, val y: Double, val z: Double) {
    operator fun plus(other: Vector): Vector = 
        Vector(x + other.x, y + other.y, z + other.z)
    
    operator fun minus(other: Vector): Vector =
        Vector(x - other.x, y - other.y, z - other.z)
    
    operator fun times(scalar: Double): Vector =
        Vector(x * scalar, y * scalar, z * scalar)
    
    infix fun dot(other: Vector): Double =
        x * other.x + y * other.y + z * other.z
    
    infix fun cross(other: Vector): Vector =
        Vector(
            y * other.z - z * other.y,
            z * other.x - x * other.z,
            x * other.y - y * other.x
        )
    
    override fun toString(): String = "($x, $y, $z)"
}

// 向量运算 DSL
fun vector(block: VectorBuilder.() -> Unit): Vector {
    val builder = VectorBuilder()
    builder.block()
    return builder.build()
}

class VectorBuilder {
    var x = 0.0
    var y = 0.0
    var z = 0.0
    
    fun x(value: Double) { x = value }
    fun y(value: Double) { y = value }
    fun z(value: Double) { z = value }
    
    fun build(): Vector = Vector(x, y, z)
}

// 使用向量 DSL
fun testVectorDSL() {
    val v1 = vector { x(1.0); y(2.0); z(3.0) }
    val v2 = vector { x(4.0); y(5.0); z(6.0) }
    
    val sum = v1 + v2
    val difference = v1 - v2
    val scaled = v1 * 2.5
    val dotProduct = v1 dot v2
    val crossProduct = v1 cross v2
    
    println("向量运算结果:")
    println("和: $sum")
    println("差: $difference")
    println("缩放: $scaled")
    println("点积: $dotProduct")
    println("叉积: $crossProduct")
}

四十四、Kotlin 元编程终极实战:构建迷你框架

import kotlin.reflect.full.*

// 迷你依赖注入框架
annotation class Inject
annotation class Singleton

class DIContainer {
    private val instances = mutableMapOf<KClass<*>, Any>()
    private val bindings = mutableMapOf<KClass<*>, KClass<*>>()
    
    fun <T : Any> register(clazz: KClass<T>) {
        instances[clazz] = createInstance(clazz)
    }
    
    fun <T : Any> register(interfaceClass: KClass<T>, implementationClass: KClass<out T>) {
        bindings[interfaceClass] = implementationClass
    }
    
    inline fun <reified T : Any> get(): T {
        return get(T::class)
    }
    
    @Suppress("UNCHECKED_CAST")
    fun <T : Any> get(clazz: KClass<T>): T {
        // 检查单例实例
        if (instances.containsKey(clazz)) {
            return instances[clazz] as T
        }
        
        // 检查接口绑定
        val targetClass = bindings[clazz] ?: clazz
        
        // 创建新实例
        val instance = createInstance(targetClass as KClass<T>)
        
        // 如果是单例,保存实例
        if (targetClass.hasAnnotation<Singleton>()) {
            instances[clazz] = instance
        }
        
        return instance
    }
    
    private fun <T : Any> createInstance(clazz: KClass<T>): T {
        val constructor = clazz.primaryConstructor
            ?: throw IllegalArgumentException("类 ${clazz.simpleName} 没有主构造函数")
        
        val parameters = constructor.parameters.map { param ->
            if (param.hasAnnotation<Inject>()) {
                get(param.type.classifier as KClass<*>)
            } else {
                throw IllegalArgumentException("无法解析依赖: ${param.name}")
            }
        }
        
        return constructor.call(*parameters.toTypedArray())
    }
}

// 使用迷你 DI 框架
interface UserRepository {
    fun findUser(id: Int): String
}

@Singleton
class DatabaseUserRepository @Inject constructor() : UserRepository {
    override fun findUser(id: Int): String = "用户 $id 来自数据库"
}

interface UserService {
    fun getUser(id: Int): String
}

@Singleton
class UserServiceImpl @Inject constructor(
    private val userRepository: UserRepository
) : UserService {
    override fun getUser(id: Int): String {
        return "服务: ${userRepository.findUser(id)}"
    }
}

class UserController @Inject constructor(
    private val userService: UserService
) {
    fun showUser(id: Int): String {
        return "控制器: ${userService.getUser(id)}"
    }
}

// 测试迷你框架
fun testMiniDI() {
    val container = DIContainer()
    
    // 注册依赖
    container.register(UserRepository::class, DatabaseUserRepository::class)
    container.register(UserService::class, UserServiceImpl::class)
    container.register(UserController::class)
    
    // 使用依赖注入
    val controller = container.get<UserController>()
    val result = controller.showUser(1)
    
    println("DI 框架测试结果: $result")
    
    // 验证单例模式
    val repo1 = container.get<UserRepository>()
    val repo2 = container.get<UserRepository>()
    println("单例验证: ${repo1 === repo2}") // 应该输出 true
}

四十五、Kotlin 性能监控与调试

// 性能监控工具类
object PerformanceMonitor {
    private val timings = mutableMapOf<String, Long>()
    private val counters = mutableMapOf<String, Int>()
    
    inline fun <T> measure(operation: String, block: () -> T): T {
        val startTime = System.nanoTime()
        try {
            return block()
        } finally {
            val duration = System.nanoTime() - startTime
            timings.merge(operation, duration) { old, new -> (old + new) / 2 }
            counters.merge(operation, 1) { old, new -> old + new }
            println("$operation 耗时: ${duration / 1_000_000}ms")
        }
    }
    
    fun printStatistics() {
        println("\n=== 性能统计 ===")
        timings.forEach { (operation, avgTime) ->
            val count = counters[operation] ?: 0
            println("$operation: 平均 ${avgTime / 1_000_000}ms (调用 $count 次)")
        }
    }
}

// 使用性能监控
fun testPerformanceMonitoring() {
    PerformanceMonitor.measure("数据加载") {
        Thread.sleep(500) // 模拟耗时操作
    }
    
    PerformanceMonitor.measure("数据处理") {
        repeat(1000) { i ->
            PerformanceMonitor.measure("内部操作") {
                // 模拟内部操作
                Math.sqrt(i.toDouble())
            }
        }
    }
    
    PerformanceMonitor.printStatistics()
}

下一步:成为 Kotlin 专家

你现在已经掌握了 Kotlin 的终极技能!接下来可以:

  1. 参与 Kotlin 编译器开发:贡献给 Kotlin 语言本身
  2. 开发 Kotlin 编译器插件:创建自定义语言特性
  3. 研究 Kotlin 编译器内部机制:理解类型推断、代码生成
  4. 探索 Kotlin 多平台前沿:实验性特性和新平台支持
  5. 贡献开源 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项目,提高我们自己的核心竞争力,加油吧,前端工程师们。

简单题,简单做(Python/Java/C++/C/Go/JS/Rust)

作者 endlesscheng
2025年11月24日 06:20

题意:计算 $\textit{nums}$ 每个前缀的二进制数值 $x$,判断 $x$ 是否为 $5$ 的倍数。

比如 $\textit{nums}=[1,1,0,1]$,每个前缀对应的二进制数分别为 $1,11,110,1101$。

如何计算这些二进制数呢?

在十进制中,我们往 $12$ 的右边添加 $3$,得到 $123$,做法是 $12\cdot 10 + 3 = 123$。

对于二进制,做法类似,往 $110$ 的右边添加 $1$,得到 $1101$,做法是 $110\cdot 2 + 1 = 1101$,或者 $110\ \texttt{<<}\ 1\ |\ 1 = 1101$。

注意本题 $\textit{nums}$ 很长,算出的二进制数 $x$ 很大,但我们只需要判断 $x\bmod 5=0$ 是否成立。可以在中途取模,也就是每次循环计算出新的 $x$ 后,把 $x$ 替换成 $x\bmod 5$。为什么可以在中途取模?原理见 模运算的世界:当加减乘除遇上取模

###py

class Solution:
    def prefixesDivBy5(self, nums: List[int]) -> List[bool]:
        ans = [False] * len(nums)
        x = 0
        for i, bit in enumerate(nums):
            x = (x << 1 | bit) % 5
            ans[i] = x == 0
        return ans

###java

class Solution {
    public List<Boolean> prefixesDivBy5(int[] nums) {
        List<Boolean> ans = new ArrayList<>(nums.length); // 预分配空间
        int x = 0;
        for (int bit : nums) {
            x = (x << 1 | bit) % 5;
            ans.add(x == 0);
        }
        return ans;
    }
}

###cpp

class Solution {
public:
    vector<bool> prefixesDivBy5(vector<int>& nums) {
        vector<bool> ans(nums.size());
        int x = 0;
        for (int i = 0; i < nums.size(); i++) {
            x = (x << 1 | nums[i]) % 5;
            ans[i] = x == 0;
        }
        return ans;
    }
};

###c

bool* prefixesDivBy5(int* nums, int numsSize, int* returnSize) {
    *returnSize = numsSize;
    bool* ans = malloc(numsSize * sizeof(bool));
    int x = 0;
    for (int i = 0; i < numsSize; i++) {
        x = (x << 1 | nums[i]) % 5;
        ans[i] = x == 0;
    }
    return ans;
}

###go

func prefixesDivBy5(nums []int) []bool {
ans := make([]bool, len(nums))
x := 0
for i, bit := range nums {
x = (x<<1 | bit) % 5
ans[i] = x == 0
}
return ans
}

###js

var prefixesDivBy5 = function(nums) {
    const ans = new Array(nums.length);
    let x = 0;
    for (let i = 0; i < nums.length; i++) {
        x = ((x << 1) | nums[i]) % 5;
        ans[i] = x === 0;
    }
    return ans;
};

###rust

impl Solution {
    pub fn prefixes_div_by5(nums: Vec<i32>) -> Vec<bool> {
        let mut ans = vec![false; nums.len()];
        let mut x = 0;
        for (i, bit) in nums.into_iter().enumerate() {
            x = (x << 1 | bit) % 5;
            ans[i] = x == 0;
        }
        ans
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$,其中 $n$ 是 $\textit{nums}$ 的长度。
  • 空间复杂度:$\mathcal{O}(1)$。返回值不计入。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

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

❌
❌