普通视图

发现新文章,点击刷新页面。
今天 — 2026年1月26日掘金 前端

8.BTC-挖矿-北大肖臻老师客堂笔记

作者 前端涂涂
2026年1月26日 16:32

这段视频是《区块链技术与应用》第 08 讲“BTC 挖矿”的内容,重点讲清楚:什么是全节点、矿工具体做什么、挖矿流程和策略,以及和前面“难度、工作量证明”的关系。


一、全节点的职责

视频先从“全节点”说起,说明什么样的节点才算比特币系统中的全节点。

  • 一直在线:长时间运行程序、参与网络协议、持续收发区块和交易。
  • 本地维护完整区块链:磁盘里保存从创世区块到当前高度的全部区块数据。
  • 维护 UTXO 集合:在内存中维护当前“未花费输出集合”(UTXO set),用来快速验证转账是否真的有钱可花。UTXO 是检测双花(double spending)的关键数据结构。
  • 监听并验证交易:从 P2P 网络中接收交易,对签名、余额、格式等做合法性检查,丢弃非法交易。
  • 决定打包哪些交易:把合法交易放入本地的交易池(mempool),再从中挑选交易准备打包进新区块,一般优先手续费高的交易。
  • 监听并验证区块:收到其他矿工挖出的新区块后,检查区块头、工作量证明(难度目标)、区块内所有交易是否合法,决定是否接受这个区块并接到当前最长合法链上。 全节点是“规则的执行者和裁判”,哪怕不挖矿,只要运行全节点就能帮助网络一起维护协议的正确性。

二、矿工在全节点基础上的额外工作

矿工本质上是“带挖矿功能的全节点”,在普通全节点的基础上,多做了几件事。

  • 选择要延长哪条链:
    • 当存在多条合法链分支时,矿工按照“最长合法链”原则选择一个分支作为当前主链的末尾,在该链尾部继续挖新区块。
  • 构造候选区块:
    • 从本地 UTXO 和 mempool 中挑出一批交易,组合成一个完整区块(区块头 + 区块体),并在区块体中加入一笔“铸币交易”(coinbase),把区块奖励和手续费打给自己控制的地址。
  • 做工作量证明(PoW):
    • 不断修改区块头中的随机数(nonce)或其他可调字段,反复计算区块头哈希,直到找到一个小于当前难度目标的哈希值,即完成挖矿。

因此,矿工的核心任务可以概括为两步:
1)选链;2)在所选链上构造合法区块并做 PoW 计算。


三、挖矿的具体流程

视频对“矿工从接收交易到挖出区块”的过程做了较完整的串联。

  1. 接收交易并存入 mempool

    • 矿工节点从网络接收交易,对每笔交易做完整验证(签名、余额、格式等),合法的放入本地交易池,非法的直接丢弃,不再转发。
  2. 决定区块内容

    • 按一定策略(一般是优先手续费更高的交易)从 mempool 中选出一批交易,注意总大小不能超过区块大小上限。 blog.csdn
    • 在这些交易前加入一笔 coinbase 交易,指定奖励的接收地址(矿工自己的地址)。
  3. 构造区块头

    • 设置前一个区块的哈希(指向父区块)、Merkle 根、时间戳、难度目标等字段,形成一个待挖的区块头。
  4. 执行 PoW

    • 不断修改 nonce 或额外字段,重复计算哈希,直到满足难度要求为止。
    • 这是一个完全概率性的过程,算力越大,找到符合条件哈希的概率越高。
  5. 广播新区块

    • 找到合法哈希后,矿工将新区块广播给网络中其他节点,对方验证通过后,将该区块接到各自的最长合法链上,并继续在这个新区块上向前挖矿。

四、分支与“沿哪条链挖下去”的策略

视频强调了一点:由于网络延迟与挖矿的随机性,比特币系统中临时出现“多分支”的情况是正常现象。

  • 同时挖出两个区块:
    • 不同矿工几乎同时在同一个高度挖出两个区块,部分节点先收到 A 区块,部分节点先收到 B 区块,于是形成两个长度相同的合法分支。
  • 矿工的策略:
    • 每个矿工总是对“当前看到的最长合法链”的末尾进行挖矿,即只在自己认为的主链上继续延长。
  • 分支的消解:
    • 之后某个分支先挖出下一个区块,就变成更长的链,网络中大多数节点会随之切换到更长的分支,另一条短分支变成“孤块链”,其区块奖励作废、对应交易需要重新被打包。

这一策略保证了长远来看全网会逐渐收敛到一个主链,但短期内允许存在暂时的不一致。


五、难度、收益与矿工行为(与前一讲的衔接)

虽然“难度、出块时间”等细节主要在上一讲“挖矿难度”里展开,但本视频在讲挖矿行为时有若干关键承接点。

  • 出块时间和难度调节:
    • 比特币系统以 10 分钟左右的平均出块时间为目标,通过全网算力与难度调整机制,使整体出块速度在长期统计上保持稳定。
  • 矿工收益构成:
    • 每个新区块带有固定区块奖励 + 区块中所有交易的手续费,合计收益通过 coinbase 交易发给挖出区块的矿工。
  • 矿工的激励与安全性:
    • 因为要投入大量算力与电费,矿工有动机遵守协议(否则挖出的非法区块会被全节点拒绝,无法获得奖励)。
    • 大部分算力诚实执行规则时,比特币的安全性才能得到保障,这是 PoW 机制设计的根本出发点之一。

六、视频的整体逻辑结构总结

可以把整个第 08 讲“BTC 挖矿”总结为下面的结构:

  1. 先界定“全节点”的概念和职责:维护区块链、UTXO、验证交易与区块。
  2. 再在此基础上定义“矿工”:全节点 + 决定打包交易 + 执行 PoW 的节点。
  3. 详细讲挖矿流程:从接收交易、构造区块、PoW 计算,到广播新区块。
  4. 解释有分支时矿工如何选链、为什么会出现分叉,以及系统如何自然消解大多数临时分叉。
  5. 与上一讲的难度调整、收益机制衔接,强调挖矿行为背后的激励与安全性含义。

在这里插入图片描述转存失败,建议直接上传图片文件

一文看懂Vue2 与 Vue3 组件传参

2026年1月26日 16:16

Vue2 组件通信方式

  1. Props(父传子)
<!-- 父组件 Parent.vue -->
<template>
  <Child :title="parentTitle" :user="userData" />
</template>

<script>
import Child from './Child.vue'
export default {
  components: { Child },
  data() {
    return {
      parentTitle: 'Hello from Parent',
      userData: { name: 'John', age: 25 }
    }
  }
}
</script>

<!-- 子组件 Child.vue -->
<template>
  <div>
    <h2>{{ title }}</h2>
    <p>Name: {{ user.name }}</p >
  </div>
</template>

<script>
export default {
  props: {
    // 基础类型
    title: {
      type: String,
      required: true,
      default: 'Default Title'
    },
    // 对象类型
    user: {
      type: Object,
      default: () => ({})
    }
  }
}
</script>
  1. $emit(子传父)
<!-- 子组件 Child.vue -->
<template>
  <button @click="sendMessage">发送消息给父组件</button>
</template>

<script>
export default {
  methods: {
    sendMessage() {
      this.$emit('message-from-child', 'Hello Parent!')
    }
  }
}
</script>

<!-- 父组件 Parent.vue -->
<template>
  <Child @message-from-child="handleChildMessage" />
</template>

<script>
export default {
  methods: {
    handleChildMessage(msg) {
      console.log('收到子组件消息:', msg)
    }
  }
}
</script>
  1. Event Bus(全局事件总线)
// eventBus.js
import Vue from 'vue'
export const EventBus = new Vue()

// 组件A - 发送事件
<script>
import { EventBus } from './eventBus'
export default {
  methods: {
    sendData() {
      EventBus.$emit('global-event', { data: 'some data' })
    }
  }
}
</script>

// 组件B - 接收事件
<script>
import { EventBus } from './eventBus'
export default {
  created() {
    EventBus.$on('global-event', (payload) => {
      console.log('收到事件:', payload)
    })
  },
  beforeDestroy() {
    EventBus.$off('global-event')
  }
}
</script>
  1. Vuex(状态管理)
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment(state, payload) {
      state.count += payload.amount
    }
  },
  actions: {
    incrementAsync({ commit }, payload) {
      setTimeout(() => {
        commit('increment', payload)
      }, 1000)
    }
  }
})

// 组件中使用
<script>
export default {
  computed: {
    count() {
      return this.$store.state.count
    }
  },
  methods: {
    increment() {
      this.$store.commit('increment', { amount: 1 })
      // 或使用actions
      this.$store.dispatch('incrementAsync', { amount: 1 })
    }
  }
}
</script>
  1. $attrs$listeners
<!-- 父组件 Parent.vue -->
<template>
  <Child 
    title="传递的标题"
    :data="someData"
    @custom-event="handleEvent"
  />
</template>

<!-- 中间组件 Child.vue -->
<template>
  <!-- 传递所有属性和事件 -->
  <GrandChild v-bind="$attrs" v-on="$listeners" />
</template>

<script>
export default {
  inheritAttrs: false // 不将attrs设置为DOM属性
}
</script>

<!-- 最终组件 GrandChild.vue -->
<template>
  <div>
    <h2>{{ title }}</h2>
    <button @click="$emit('custom-event')">触发事件</button>
  </div>
</template>

<script>
export default {
  props: ['title']
}
</script>

Vue3 组件通信方式

  1. Props(父传子) - Composition API
<!-- 父组件 Parent.vue -->
<template>
  <Child 
    :title="parentTitle" 
    :user="userData"
    @update:title="parentTitle = $event"
  />
</template>

<script setup>
import { ref } from 'vue'
import Child from './Child.vue'

const parentTitle = ref('Hello from Parent')
const userData = ref({ name: 'John', age: 25 })
</script>

<!-- 子组件 Child.vue -->
<template>
  <div>
    <h2>{{ title }}</h2>
    <button @click="updateTitle">修改标题</button>
  </div>
</template>

<script setup>
import { defineProps, defineEmits } from 'vue'

const props = defineProps({
  title: {
    type: String,
    required: true,
    default: 'Default Title'
  },
  user: {
    type: Object,
    default: () => ({})
  }
})

const emit = defineEmits(['update:title'])

const updateTitle = () => {
  emit('update:title', 'New Title from Child')
}
</script>
  1. defineEmits(子传父)
<!-- 子组件 Child.vue -->
<template>
  <button @click="sendData">发送数据</button>
</template>

<script setup>
import { defineEmits } from 'vue'

const emit = defineEmits({
  // 带验证的emit
  'send-message': (payload) => {
    return typeof payload === 'string'
  }
})

const sendData = () => {
  emit('send-message', 'Hello from Child')
  emit('update:count', 10) // 支持v-model语法
}
</script>

<!-- 父组件 Parent.vue -->
<template>
  <Child 
    @send-message="handleMessage"
    @update:count="count = $event"
  />
</template>

<script setup>
import { ref } from 'vue'

const count = ref(0)

const handleMessage = (msg) => {
  console.log('收到:', msg)
}
</script>
  1. provide/inject(跨层级通信)
<!-- 祖先组件 Ancestor.vue -->
<template>
  <Parent />
</template>

<script setup>
import { provide, ref } from 'vue'
import Parent from './Parent.vue'

const sharedData = ref('共享数据')
const updateSharedData = (newValue) => {
  sharedData.value = newValue
}

// 提供数据和方法
provide('sharedData', {
  sharedData,
  updateSharedData
})
</script>

<!-- 后代组件 Descendant.vue -->
<template>
  <div>{{ data.sharedData }}</div>
</template>

<script setup>
import { inject } from 'vue'

const data = inject('sharedData')

const updateData = () => {
  data.updateSharedData('新的共享数据')
}
</script>
  1. Pinia(Vue3推荐的状态管理)
// stores/counter.js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),
  actions: {
    increment(amount = 1) {
      this.count += amount
    }
  },
  getters: {
    doubleCount: (state) => state.count * 2
  }
})

// 组件中使用
<template>
  <div>{{ store.count }}</div>
  <div>双倍: {{ store.doubleCount }}</div>
  <button @click="store.increment()">增加</button>
</template>

<script setup>
import { useCounterStore } from '@/stores/counter'

const store = useCounterStore()
</script>
  1. 模板引用和 defineExpose
<!-- 父组件 Parent.vue -->
<template>
  <Child ref="childRef" />
  <button @click="callChildMethod">调用子组件方法</button>
</template>

<script setup>
import { ref } from 'vue'
import Child from './Child.vue'

const childRef = ref(null)

const callChildMethod = () => {
  childRef.value?.childMethod()
  console.log('子组件数据:', childRef.value?.childData)
}
</script>

<!-- 子组件 Child.vue -->
<template>
  <div>子组件</div>
</template>

<script setup>
import { ref, defineExpose } from 'vue'

const childData = ref('子组件数据')

const childMethod = () => {
  console.log('子组件方法被调用')
}

// 暴露给父组件
defineExpose({
  childData,
  childMethod
})
</script>
  1. 自定义Hooks(组合式函数)
// composables/useCounter.js
import { ref, computed } from 'vue'

export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  
  const increment = () => count.value++
  const decrement = () => count.value--
  const double = computed(() => count.value * 2)
  
  return {
    count,
    increment,
    decrement,
    double
  }
}

// 组件中使用
<script setup>
import { useCounter } from '@/composables/useCounter'

const { count, increment, double } = useCounter(10)
</script>

Vue2 vs Vue3 通信方式对比

特性 Vue2 Vue3
Props声明 props: {} 选项 defineProps() 组合式API
事件发射 this.$emit() defineEmits()
跨组件通信 provide/inject 选项 provide()/inject() 函数
状态管理 Vuex Pinia(推荐)或 Vuex 4
暴露组件方法 自动暴露public方法 defineExpose() 显式暴露
属性透传 attrsattrs 和 listeners useAttrs() 和 useListeners()
响应式数据 data() 返回对象 ref() 或 reactive()
全局事件总线 需要创建Vue实例 使用Mitt等第三方库

最佳实践建议

Vue2项目:

  • 简单父子通信使用 props/$emit
  • 复杂状态管理使用 Vuex
  • 跨层级组件使用 provide/inject
  • 事件总线适合简单场景,注意及时清理

Vue3项目:

  • 优先使用组合式API
  • 状态管理推荐 Pinia
  • 使用TypeScript增强类型安全
  • 复杂逻辑抽离为组合式函数
  • 注意响应式数据的正确使用

Set和Map数据结构

作者 Soler
2026年1月26日 16:12

Set 数据结构

定义Set 是一种类似数组的数据结构,但其成员的值都是唯一的(自动去重)。

创建与传参:作为构造函数,它可以接收任何具有 iterable 接口的数据结构(如数组、字符串)作为参数来初始化。

javascript

new Set([1, 2, 3]); // 来自数组
new Set('abc'); // 来自字符串

实例属性和方法

属性/方法 说明与示例 返回值
set.size 返回Set实例的成员总数。 Number
set.add(value) 添加某个值,返回Set本身(支持链式调用)。 Set 对象
set.has(value) 判断该值是否为Set的成员。 Boolean
set.delete(value) 删除某个值。 Boolean(表示是否删除成功)
set.clear() 清除所有成员。 undefined

Set 与数组的相互转换

  1. Set => 数组(最常用的去重方法):

    javascript

    // 方法1:扩展运算符
    let arr1 = [...new Set([1, 2, 2, 4])]; // [1, 2, 4]
    // 方法2:Array.from
    let arr2 = Array.from(new Set([1, 2, 4]));
    
  2. 数组 => Set

    javascript

    new Set([1, 2, 3]); // 自动去重
    

遍历方法

Set 结构的键名和键值是同一个值,因此 keys() 和 values() 的行为完全一致。

  • Set.prototype.keys():返回键名的遍历器。
  • Set.prototype.values():返回键值的遍历器。
  • Set.prototype.entries():返回键值对的遍历器(每项为 [value, value])。
  • Set.prototype.forEach():使用回调函数遍历每个成员。回调函数参数为 (value, sameValue, set)

WeakSet

定义与特点

  • 成员只能是对象(或后文提到的Symbol值,但主流环境通常仅支持对象),不能是其他类型的值。
  • 对成员对象是弱引用。这意味着,如果没有其他变量引用该对象,即使它存在于 WeakSet 中,垃圾回收机制也会自动回收其占用的内存。
  • 由于成员可能随时被回收,WeakSet 没有 size 属性,也无法被遍历

创建与传参
构造函数接受的参数必须是一个可迭代对象,且其每个成员都是对象。通常使用二维数组,因为内层数组的成员才是 WeakSet 的值

javascript

// 正确:二维数组,成员[1,2][3,4]是对象(数组是引用类型)
new WeakSet([[1, 2], [3, 4]]);
// 错误:一维数组成员不是对象
// new WeakSet([1, 2, 3]);

弱引用示例

javascript

let obj = {name: 'qin'};
const ws = new WeakSet([obj]);
obj = null; // 此后,{name: 'qin'} 对象可能被垃圾回收,无法从 ws 中取回。

实例方法

  • WeakSet.prototype.add(object):添加对象成员。
  • WeakSet.prototype.has(object):判断对象是否存在。
  • WeakSet.prototype.delete(object):删除对象成员。

Map 数据结构

定义Map 是一种完善的“键值对”集合数据结构。与传统对象(Object)只能用字符串或 Symbol 作为键不同,Map 的“键”可以是任何类型的值(包括对象、函数)。

创建与传参
构造函数可以接受一个可迭代对象作为参数,该对象的每个成员必须是一个表示键值对的二元数组

javascript

// 正确:传入一个二维数组,每个子数组是[key, value]
new Map([['name', 'qin'], [{id: 1}, 'objectKey']]);

实例属性和方法

属性/方法 说明与示例 返回值
map.size 返回 Map 实例的成员总数。 Number
map.set(key, value) 设置键值对。返回Map本身,支持链式调用 Map 对象
map.get(key) 读取指定键对应的值。 value 或 undefined
map.has(key) 判断是否有指定的键。 Boolean
map.delete(key) 删除某个键。 Boolean
map.clear() 清除所有成员。 undefined

遍历方法

  • Map.prototype.keys():返回键名的遍历器。
  • Map.prototype.values():返回键值的遍历器。
  • Map.prototype.entries():返回所有成员(键值对)  的遍历器。
  • Map.prototype.forEach():遍历所有成员。回调函数参数为 (value, key, map)

与其他数据结构的转换

  1. Map => 数组(使用扩展运算符最方便):

    javascript

    const myMap = new Map().set('a', 1).set('b', 2);
    console.log([...myMap]); // [ ['a', 1], ['b', 2] ]
    
  2. 对象 => Map(借助 Object.entries):

    javascript

    let obj = {"a": 1, "b": 2};
    let map = new Map(Object.entries(obj));
    
  3. Map => 对象(当Map的键都是字符串时):

    javascript

    function mapToObj(map) {
      let obj = Object.create(null);
      for (let [k, v] of map) {
        obj[k] = v;
      }
      return obj;
    }
    

WeakMap

定义与特点

  • 只接受对象(null除外)  作为键名,不接受其他类型的值。
  • 对键名所指向的对象是弱引用,不影响垃圾回收。键名对象被回收后,对应的键值对会自动从 WeakMap 中移除。
  • 因此,WeakMap 没有 size 属性,也无法被遍历
  • 应用场景:常用于需要将额外数据与对象关联,又不想影响对象本身生命周期的情况(如DOM元素作为键名存储元数据)。

对象的迭代

普通对象 {} 默认是不可迭代的,不能直接用于 for...of 循环。

遍历普通对象的几种方式:

  1. for...in 循环(遍历自身和继承的可枚举属性键名,通常需搭配 hasOwnProperty 过滤):

    javascript

    for (const key in obj) {
      if (obj.hasOwnProperty(key)) {
        console.log(key, obj[key]);
      }
    }
    
  2. Object.keys() + for...of(遍历自身可枚举属性键名):

    javascript

    for (const key of Object.keys(obj)) {
      console.log(key, obj[key]);
    }
    
  3. Object.values() + for...of(遍历自身可枚举属性值):

    javascript

    for (const value of Object.values(obj)) {
      console.log(value);
    }
    
  4. Object.entries() + for...of(遍历自身可枚举属性键值对):

    javascript

    for (const [key, value] of Object.entries(obj)) {
      console.log(key, value);
    }
    

让普通对象可迭代

通过自定义 [Symbol.iterator] 方法,可以使普通对象支持 for...of 循环。

javascript

const obj = {
  a: 1,
  b: 2,
  [Symbol.iterator]: function* () {
    yield* Object.entries(this); // 使用生成器函数委托给 entries
  }
};
for (const [key, value] of obj) { // 现在可以 for...of 了
  console.log(key, value);
}

总结

特性 Set Map WeakSet WeakMap
核心特点 值唯一的集合 键值对集合,键类型任意 弱引用对象集合 弱引用对象作为键的集合
键/值类型 值可以是任何类型 键可以是任何类型 值必须是对象 键必须是对象
可遍历性
size 属性
垃圾回收影响 强引用,会阻止成员被回收 强引用,会阻止键被回收 弱引用,不影响成员被回收 弱引用,不影响键对象被回收

vue3 源代码reactive 到底干了什么?

2026年1月26日 16:12
packages\reactivity\src\reactive.ts

在 reactive.ts 里搜索 export function reactive(大概在 80 行左右)。


function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>,
) {
  if (!isObject(target)) {
    if (__DEV__) {
      warn(
        `value cannot be made ${isReadonly ? 'readonly' : 'reactive'}: ${String(
          target,
        )}`,
      )
    }
    return target
  }
  // target is already a Proxy, return it.
  // exception: calling readonly() on a reactive object
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }
  // only specific value types can be observed.
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }
  // target already has corresponding Proxy
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers,
  )
  proxyMap.set(target, proxy)
  return proxy
}

我把它拆解为 5 个关键步骤。

第一步:身份检查(只加工对象)

if (!isObject(target)) {
  if (__DEV__) {
    warn(`value cannot be made ...`)
  }
  return target
}

大白话:Proxy 只能代理“对象”(Object、Array、Map、Set 等)。如果你传个数字 123 或字符串 "hello" 进来,Vue 直接无视你,把原值返回,并在开发环境下弹个警告。

这就是为什么:如果你想让一个基本类型变成响应式,得用 ref 而不是 reactive。

第二步:防止重复加工(已经是 Proxy 了吗?)

if (
  target[ReactiveFlags.RAW] &&
  !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
  return target
}

大白话:如果你拿一个已经是 Proxy 的东西再丢给 reactive(),它会直接还给你。

细节:target[ReactiveFlags.RAW] 是一个特殊的标记。如果 target 是 Proxy,它能识别这个标记并返回原对象。

例外:有一种情况允许重复加工——把一个响应式对象变成“只读”对象(即 readonly(reactive(obj)) 是允许的)。

第三步:黑名单检查(它能被加工吗?)

const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
  return target
}

大白话:有些对象是 Vue 不想触碰的。

谁在黑名单里?

被 Object.freeze() 冻结的对象。

特殊的内置对象(如 RegExp, Date, Promise)。

带有 __v_skip: true 标记的对象(你可以手动让某个大对象跳过响应式)。

结论:如果是在黑名单里的对象,原样返回。

第四步:查缓存(别重复造轮子)

const existingProxy = proxyMap.get(target)
if (existingProxy) {
  return existingProxy
}

大白话:Vue 内部维护了一张表(proxyMap,是一个 WeakMap)。

场景


const obj = { name: 'vue' }
const p1 = reactive(obj)
const p2 = reactive(obj) // 再次调用

当第二次调用 reactive(obj) 时,Vue 发现 obj 已经在表里了,直接把上次造好的 p1 返回给你。这样能保证 p1 === p2,节省性能。

第五步:正式合体(最关键的一步)

const proxy = new Proxy(
  target,
  targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers,
)
proxyMap.set(target, proxy)
return proxy

大白话:这是真正干活的地方。

new Proxy(target, handlers):

target: 原始对象。

handlers: 这是响应式的灵魂。它定义了当你“读取”或“修改”对象时,要做什么操作(比如追踪依赖、触发更新)。

分支逻辑:

如果是 Map, Set, WeakMap, WeakSet,使用 collectionHandlers。

如果是 普通对象或数组,使用 baseHandlers(也就是你在入口看到的 mutableHandlers)。

最后:把新生成的 proxy 存入缓存表,并返回它。

总结:reactive 到底干了啥? 你只需要记住这个流程图:

传个东西进来 -> 是对象吗?(不是就滚粗)

看一眼 -> 已经是 Proxy 了吗?(是就直接还你)

再看一眼 -> 在黑名单里吗?(是就原样返回)

查查表 -> 以前加工过吗?(加工过就把旧的给你)

动手 -> 披上一层 Proxy 的外套(注入 handlers),存进表里,收工!

CSS-玩转背景控制与文字排版艺术

2026年1月26日 16:11

前言

在前端开发中,细节决定质感。如何让背景图只在内容区域显示?如何优雅地处理文字溢出?本文将带你深入探索 CSS 中关于 backgroundText 的进阶属性,让你的页面更加精致。

一、 背景属性的高级控制

1. background-clip(背景裁剪)

决定背景延伸到什么位置

  • border-box (默认):背景延伸至边框外沿(但在边框下层)。
  • padding-box:背景延伸至内边距外沿,不会显示在边框下。
  • content-box:背景只在内容区域显示。
  • text (特殊):将背景裁剪为文字的前景色(常用于制作渐变文字)。

2. background-origin(背景原点)

决定背景图片的绘制起点(即 background-position: 0 0 的位置)。

  • padding-box (默认):图片左上角对齐 padding 边缘。
  • border-box:图片左上角对齐 border 边缘。
  • content-box:图片左上角对齐内容区域边缘。

3. background-size(背景尺寸)

  • contain:保持比例缩放,确保图片完整显示在容器内(可能会有留白)。
  • cover:保持比例缩放,确保图片完全覆盖容器(可能会被裁剪)。
  • 具体数值:如 100px 50%,手动指定宽高。

4. box-decoration-break(元素断裂处的修饰)

当元素被分为几个独立的盒子(如使内联元素span跨越多行),background-break属性用来控制背景怎样在这些不同的盒子中显示,取值如下:

  • slice (默认):背景像是在一个整体上绘制后被切开,断裂处没有边框/内边距。
  • clone:每个断裂的盒子都拥有独立的背景、边框和内边距。

二、 文字排版与换行控制

1. 换行与溢出

  • word-wrap / overflow-wrap

    • normal:浏览器默认。
    • break-word:如果单词太长无法在一行显示,允许在单词内换行(更常用)。
  • word-break: break-all:强制在单词内任何字符间断行,适合处理超长连续字符。

2. text-overflow(文本溢出处理)

常用于处理单行文本超出容器:

  • clip:简单裁剪,不显示省略号。
  • ellipsis:显示省略号 ...(需配合 overflow: hiddenwhite-space: nowrap 使用)。

三、 text-decoration:文字装饰的简写艺术

现代 CSS 支持更精细的文字装饰控制: text-decoration: <line> <color> <style>

1. 线型 (Line)

  • none:去除装饰(最常用于 <a> 标签)。
  • underline:下划线。
  • overline:上划线。
  • line-through:删除线。

2. 样式 (Style)

  • solid:实线(默认)。
  • double:双线。
  • dotted:点线。
  • dashed:虚线。
  • wavy波浪线

四、 实战 tips:渐变文字效果

利用 background-clip: text,你可以轻松实现绚丽的渐变文字:

.gradient-text {
  background: linear-gradient(to right, red, blue);
  -webkit-background-clip: text; /* 必须加前缀 */
  color: transparent;           /* 文字设为透明,露出背景 */
  font-size: 40px;
}

一文搞懂现代浏览器架构(面试&入门双适用)

作者 Wect
2026年1月26日 16:09

现代浏览器(以 Chrome 为代表)早已抛弃早期的单进程设计,全面采用 多进程架构(Multi-Process Architecture)。这一设计看似复杂,核心却是为了解决实际使用中的关键问题,今天就用最通俗的方式拆解清楚,不管是入门还是面试都够用。

一、为什么浏览器非要用多进程?

先给一个面试直接能用的核心动机,记牢就行:

浏览器必须同时兼顾 稳定性、安全性、性能 三大核心需求。单进程浏览器有个致命问题——一旦崩溃,整个浏览器直接挂掉。所以现代浏览器把不同功能拆分到不同进程,实现“局部故障不影响整体”。

举个直观例子

日常使用中很容易遇到这些情况:某个网页的 JS 陷入死循环、视频播放占满显卡资源、单个页面突然崩溃。而多进程架构的核心优势就是——这些问题都不会影响其他标签页或浏览器本身,最多只是出问题的页面关闭,不耽误整体使用。

二、浏览器主要有哪些进程?(核心重点)

先记一个核心结论,帮你建立整体认知:

浏览器 ≈ 1个浏览器主进程 + 多个功能进程 + 多个渲染进程

下面逐个拆解,重点内容会特别标注,面试常考的地方也会提醒。

1. Browser Process(浏览器主进程)—— 浏览器的“大总管”

它不负责具体的页面内容渲染,只做全局管理工作,是整个浏览器的核心调度者。

主要职责:

  • 管理浏览器窗口(地址栏、书签栏、前进后退按钮等界面元素);

  • 负责标签页的创建与销毁;

  • 调度其他进程的创建、销毁与协作;

  • 处理用户输入(比如在地址栏输内容、按快捷键);

  • 管理各类权限(下载、摄像头调用、弹窗通知等)。

举个实际场景:在地址栏输入URL

  1. 浏览器主进程首先接收你的输入;

  2. 判断你输入的是搜索关键词(比如“天气”)还是具体网址(比如“www.baidu.com”);

  3. 创建或复用一个渲染进程,专门用来处理这个页面;

  4. 把URL交给网络进程,让它去发起请求获取资源。

这里一定要记住:主进程只做“调度管理”,不碰页面渲染的具体工作。

2. Renderer Process(渲染进程)—— 重点中的重点

这是面试高频考点,也是页面能正常显示的核心进程。

一句话定义

每个标签页(严格来说是每个站点实例)基本都会对应一个独立的渲染进程,实现进程隔离。

核心职责

所有和页面内容相关的工作,都由它负责:

  • 解析HTML和CSS代码;

  • 构建DOM树(HTML解析结果)和CSSOM树(CSS解析结果);

  • 执行页面中的JavaScript代码;

  • 计算页面布局、绘制页面内容;

  • 响应用户在页面内的交互(比如点击按钮、输入文字)。

内部线程(面试加分项)

渲染进程内部不是单线程工作,而是由多个线程协作:

  • 主线程:负责执行JavaScript代码,同时处理HTML/CSS解析、布局绘制等渲染工作;

  • 合成线程(Compositor):负责页面图层合成,让动画更流畅;

  • 光栅线程(Raster):负责把页面元素转换成像素,供最终显示。

举个真实场景:打开一个React页面

  1. HTML文件下载完成后,渲染进程开始解析HTML,生成DOM树;

  2. 同时下载页面所需的CSS文件,解析后生成CSSOM树;

  3. 执行页面中的JavaScript代码(包括React框架代码),通过JS操作DOM和CSSOM;

  4. 根据DOM树和CSSOM树计算页面布局,确定每个元素的位置和样式;

  5. 绘制页面内容,再通过合成线程、光栅线程处理后,呈现到屏幕上;

  6. 当用户点击页面按钮时,主线程执行对应的JS回调,响应用户交互。

这里有个高频考点:JavaScript会阻塞页面渲染,本质就是因为JS执行和渲染工作共享同一个主线程,JS执行时会占用主线程资源,导致渲染工作暂停。

为什么要做渲染进程隔离?

主要为了安全和稳定,两点原因很明确:

  • 安全层面:JavaScript属于不可信代码,每个站点一个渲染进程相当于一个“沙箱”,能防止跨站点窃取数据;

  • 稳定层面:某个页面崩溃(比如JS死循环),只会影响对应的渲染进程,不会拖垮其他标签页或浏览器本身。

面试常问:一个标签页就对应一个进程吗?

标准回答(记牢不踩坑):

  • 大多数情况下是这样的;

  • Chrome有个“Site Isolation(站点隔离)”机制;

  • 同源页面(比如同一个网站的不同页面)可能会复用同一个渲染进程,节省资源;

  • 跨站点页面(不同域名)通常会使用独立的渲染进程,保证隔离性。

3. Network Process(网络进程)—— 网络请求的“专属管家”

核心职责

统一管理浏览器所有的网络请求,避免重复工作,提升效率:

  • 处理HTTP、HTTPS等协议的请求;

  • 进行DNS查询(把域名转换成IP地址);

  • 建立和管理TCP/TLS连接;

  • 管理网络缓存(已下载的资源缓存起来,下次无需重复下载)。

为什么要单独拆成一个进程?

网络请求是浏览器的高频操作,且属于共享资源。单独拆分后,多个标签页可以复用同一个网络连接,避免每个页面都单独建立连接,大幅降低资源消耗。

举个例子

同时打开3个标签页,都请求“api.example.com”这个接口。此时网络进程会复用同一个TCP连接,而不是建立3个独立连接,减少网络开销和延迟。

4. GPU Process(GPU进程)—— 图形渲染的“加速器”

核心作用

负责图形渲染加速,让页面动画、视频播放更流畅:

  • 参与页面图层合成,提升渲染效率;

  • 处理CSS动画、transform等属性的视觉效果;

  • 支持WebGL绘图、视频解码等图形密集型操作。

举个例子

当你给元素加了这样的CSS:


div {
  transform: translateZ(0);
}

这句话会触发GPU加速,让div的动画更流畅。本质就是GPU进程参与了这个元素的图层合成,减轻了主线程的压力。

为什么要独立成进程?

GPU操作属于图形密集型工作,容易出现崩溃或卡顿。单独隔离成进程后,即使GPU进程出问题,也只会影响图形渲染,不会导致整个浏览器崩溃。

5. 其他辅助进程(了解即可,面试很少问)

除了上面4个核心进程,浏览器还有一些辅助进程,负责特定场景的功能:

  • 插件进程:比如Chrome的PDF预览插件、Flash插件(现在基本淘汰),单独隔离避免插件崩溃影响浏览器;

  • 扩展进程:管理Chrome浏览器插件(比如广告拦截插件、密码管理器);

  • 辅助工具进程:处理一些小众功能,比如桌面通知、文件打印等。

三、完整流程串讲(面试最爱考,直接背)

把上面的进程串联起来,还原从输入URL到页面显示的完整过程,面试时这样讲,逻辑清晰又加分:

  1. 用户在浏览器地址栏输入URL;

  2. 浏览器主进程接收输入,判断是搜索还是URL,同时创建/复用一个渲染进程;

  3. 主进程把URL交给网络进程,让网络进程发起请求;

  4. 网络进程进行DNS查询、建立TCP连接,获取页面资源(HTML、CSS、JS等),并把资源返回给渲染进程;

  5. 渲染进程解析资源、执行JS、计算布局、绘制页面,期间GPU进程参与图层合成,提升渲染流畅度;

  6. 最终页面内容显示在屏幕上,渲染进程持续响应用户交互。

四、多进程vs单进程(对比题,面试必背)

对比维度 单进程浏览器 多进程浏览器
稳定性 差(一处崩溃,整个浏览器挂掉) 高(局部进程崩溃不影响整体)
安全性 差(无隔离,恶意代码易扩散) 高(进程隔离+沙箱机制)
性能 易阻塞(所有工作挤在一个线程) 更流畅(进程/线程并行协作)
内存占用 低(无需多进程资源开销) 较高(多进程需要额外内存)
面试补充一句点睛之笔:多进程浏览器本质是“用内存换稳定性和安全性”,这是现代浏览器的核心设计权衡。

五、面试常见追问速答(避免踩坑)

  • Q1:JavaScript在哪个进程执行? A:渲染进程的主线程。

  • Q2:为什么JS会阻塞页面渲染? A:因为JS执行和页面渲染共享渲染进程的主线程,同一时间只能做一件事,JS执行时会阻塞渲染工作。

  • Q3:跨域限制是浏览器还是服务器做的? A:浏览器的安全策略,由渲染进程和网络进程共同配合实现(网络进程拦截跨域请求,渲染进程限制DOM访问)。

六、过关标准(自查是否掌握)

如果能做到以下4点,说明浏览器架构这块已经掌握,面试基本不会被深挖:

  1. 能画出浏览器5个核心进程(主进程、渲染进程、网络进程、GPU进程、辅助进程);

  2. 能完整串讲“输入URL→页面显示”的全流程,明确每个进程的分工;

  3. 能解释清楚“为什么要用多进程”,以及多进程带来的优势与权衡;

  4. 能准确回答“标签页与渲染进程的关系”,以及相关面试追问。

前端-HTML-基础知识

作者 阿鑫_996
2026年1月26日 16:08

HTML 基础知识


一、HTML 概述

1.1 什么是 HTML

HTML(HyperText Markup Language,超文本标记语言)是用于创建网页的标准标记语言。

核心特点:

  • 标记语言,不是编程语言
  • 描述网页结构
  • 由浏览器解析和渲染
  • 与 CSS、JavaScript 配合使用

1.2 HTML 版本

版本 说明
HTML4 1999 年发布,经典版本
XHTML 更严格的 HTML
HTML5 2014 年发布,现代标准

二、HTML 文档结构

2.1 基本结构

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>页面标题</title>
</head>
<body>
  <!-- 页面内容 -->
</body>
</html>

2.2 文档类型声明

<!-- HTML5 -->
<!DOCTYPE html>

<!-- HTML4.01 Strict -->
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" 
  "http://www.w3.org/TR/html4/strict.dtd">

2.3 head 标签

<head>
  <!-- 字符编码 -->
  <meta charset="UTF-8">
  
  <!-- 视口设置(响应式) -->
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  
  <!-- 页面描述 -->
  <meta name="description" content="页面描述">
  
  <!-- 关键词 -->
  <meta name="keywords" content="关键词1,关键词2">
  
  <!-- 作者 -->
  <meta name="author" content="作者名">
  
  <!-- 页面标题 -->
  <title>页面标题</title>
  
  <!-- 引入 CSS -->
  <link rel="stylesheet" href="style.css">
  
  <!-- 引入图标 -->
  <link rel="icon" href="favicon.ico">
</head>

三、文本标签

3.1 标题标签

<h1>一级标题</h1>
<h2>二级标题</h2>
<h3>三级标题</h3>
<h4>四级标题</h4>
<h5>五级标题</h5>
<h6>六级标题</h6>

使用建议:

  • 一个页面只有一个 <h1>
  • 按层级使用,不要跳级
  • 用于文档结构,而非样式

3.2 段落和换行

<!-- 段落 -->
<p>这是一个段落。</p>
<p>这是另一个段落。</p>

<!-- 换行 -->
<p>第一行<br>第二行</p>

<!-- 水平线 -->
<hr>

3.3 文本格式化

<!-- 加粗 -->
<strong>重要文本</strong>
<b>加粗文本</b>

<!-- 斜体 -->
<em>强调文本</em>
<i>斜体文本</i>

<!-- 下划线 -->
<u>下划线文本</u>

<!-- 删除线 -->
<s>删除文本</s>
<del>删除文本</del>

<!-- 上标/下标 -->
H<sub>2</sub>O
E = mc<sup>2</sup>

<!-- 代码 -->
<code>console.log('Hello')</code>

<!-- 预格式化文本 -->
<pre>
  保留空格和换行
  多行文本
</pre>

3.4 引用

<!-- 短引用 -->
<q>这是一句引用</q>

<!-- 长引用 -->
<blockquote cite="https://example.com">
  这是一段长引用内容。
</blockquote>

<!-- 引用来源 -->
<cite>《书名》</cite>

四、链接和图片

4.1 链接(a 标签)

<!-- 外部链接 -->
<a href="https://www.example.com">访问网站</a>

<!-- 内部链接 -->
<a href="about.html">关于我们</a>

<!-- 锚点链接 -->
<a href="#section1">跳转到章节1</a>
<h2 id="section1">章节1</h2>

<!-- 新窗口打开 -->
<a href="https://example.com" target="_blank">新窗口打开</a>

<!-- 下载链接 -->
<a href="file.pdf" download>下载文件</a>

<!-- 邮件链接 -->
<a href="mailto:example@email.com">发送邮件</a>

<!-- 电话链接 -->
<a href="tel:+8613800138000">拨打电话</a>

4.2 图片(img 标签)

<!-- 基本用法 -->
<img src="image.jpg" alt="图片描述">

<!-- 设置尺寸 -->
<img src="image.jpg" alt="描述" width="300" height="200">

<!-- 响应式图片 -->
<img src="image.jpg" alt="描述" style="max-width: 100%; height: auto;">

<!-- 图片映射 -->
<img src="map.jpg" alt="地图" usemap="#imagemap">
<map name="imagemap">
  <area shape="rect" coords="0,0,100,100" href="page1.html" alt="区域1">
  <area shape="circle" coords="200,200,50" href="page2.html" alt="区域2">
</map>

图片属性:

  • src:图片路径(必需)
  • alt:替代文本(必需,用于可访问性)
  • width/height:宽高
  • loading="lazy":懒加载

五、列表

5.1 无序列表

<ul>
  <li>项目1</li>
  <li>项目2</li>
  <li>项目3</li>
</ul>

<!-- 嵌套列表 -->
<ul>
  <li>项目1
    <ul>
      <li>子项目1</li>
      <li>子项目2</li>
    </ul>
  </li>
  <li>项目2</li>
</ul>

5.2 有序列表

<ol>
  <li>第一项</li>
  <li>第二项</li>
  <li>第三项</li>
</ol>

<!-- 自定义起始数字 -->
<ol start="5">
  <li>第五项</li>
  <li>第六项</li>
</ol>

<!-- 倒序 -->
<ol reversed>
  <li>第三项</li>
  <li>第二项</li>
  <li>第一项</li>
</ol>

5.3 定义列表

<dl>
  <dt>术语1</dt>
  <dd>术语1的定义</dd>
  
  <dt>术语2</dt>
  <dd>术语2的定义</dd>
</dl>

六、表格

6.1 基本表格

<table>
  <tr>
    <th>表头1</th>
    <th>表头2</th>
    <th>表头3</th>
  </tr>
  <tr>
    <td>数据1</td>
    <td>数据2</td>
    <td>数据3</td>
  </tr>
  <tr>
    <td>数据4</td>
    <td>数据5</td>
    <td>数据6</td>
  </tr>
</table>

6.2 表格结构

<table>
  <!-- 表头 -->
  <thead>
    <tr>
      <th>姓名</th>
      <th>年龄</th>
      <th>城市</th>
    </tr>
  </thead>
  
  <!-- 表体 -->
  <tbody>
    <tr>
      <td>张三</td>
      <td>25</td>
      <td>北京</td>
    </tr>
    <tr>
      <td>李四</td>
      <td>30</td>
      <td>上海</td>
    </tr>
  </tbody>
  
  <!-- 表脚 -->
  <tfoot>
    <tr>
      <td colspan="3">总计:2人</td>
    </tr>
  </tfoot>
</table>

6.3 表格属性

<table border="1" cellpadding="10" cellspacing="0">
  <tr>
    <th colspan="2">合并列</th>
    <th>列3</th>
  </tr>
  <tr>
    <td rowspan="2">合并行</td>
    <td>数据1</td>
    <td>数据2</td>
  </tr>
  <tr>
    <td>数据3</td>
    <td>数据4</td>
  </tr>
</table>

七、表单

7.1 表单结构

<form action="/submit" method="POST">
  <!-- 表单控件 -->
  <input type="text" name="username">
  <button type="submit">提交</button>
</form>

7.2 输入控件

<!-- 文本输入 -->
<input type="text" name="username" placeholder="请输入用户名">

<!-- 密码输入 -->
<input type="password" name="password" placeholder="请输入密码">

<!-- 邮箱 -->
<input type="email" name="email" placeholder="example@email.com">

<!-- 数字 -->
<input type="number" name="age" min="0" max="120">

<!-- 日期 -->
<input type="date" name="birthday">

<!-- 时间 -->
<input type="time" name="time">

<!-- 颜色选择 -->
<input type="color" name="color">

<!-- 文件上传 -->
<input type="file" name="file" accept="image/*">

<!-- 隐藏字段 -->
<input type="hidden" name="token" value="abc123">

<!-- 只读 -->
<input type="text" name="readonly" value="只读" readonly>

<!-- 禁用 -->
<input type="text" name="disabled" value="禁用" disabled>

7.3 单选和复选框

<!-- 单选按钮 -->
<input type="radio" name="gender" value="male" id="male">
<label for="male"></label>
<input type="radio" name="gender" value="female" id="female">
<label for="female"></label>

<!-- 复选框 -->
<input type="checkbox" name="hobby" value="reading" id="reading">
<label for="reading">阅读</label>
<input type="checkbox" name="hobby" value="sports" id="sports">
<label for="sports">运动</label>

7.4 下拉选择

<!-- 单选下拉 -->
<select name="city">
  <option value="">请选择城市</option>
  <option value="beijing">北京</option>
  <option value="shanghai">上海</option>
  <option value="guangzhou">广州</option>
</select>

<!-- 多选下拉 -->
<select name="skills" multiple>
  <option value="html">HTML</option>
  <option value="css">CSS</option>
  <option value="js">JavaScript</option>
</select>

7.5 文本域

<!-- 多行文本 -->
<textarea name="message" rows="5" cols="30" placeholder="请输入留言"></textarea>

7.6 按钮

<!-- 提交按钮 -->
<button type="submit">提交</button>

<!-- 重置按钮 -->
<button type="reset">重置</button>

<!-- 普通按钮 -->
<button type="button">点击</button>

<!-- input 按钮 -->
<input type="submit" value="提交">
<input type="reset" value="重置">
<input type="button" value="按钮">

7.7 表单验证

<form>
  <!-- 必填 -->
  <input type="text" name="name" required>
  
  <!-- 最小/最大长度 -->
  <input type="text" name="username" minlength="3" maxlength="20">
  
  <!-- 正则验证 -->
  <input type="text" name="phone" pattern="[0-9]{11}" 
    title="请输入11位手机号">
  
  <!-- 数值范围 -->
  <input type="number" name="age" min="18" max="100">
  
  <!-- 提交时验证 -->
  <button type="submit">提交</button>
</form>

八、HTML5 语义化标签

8.1 结构标签

<header>
  <h1>网站标题</h1>
  <nav>
    <ul>
      <li><a href="/">首页</a></li>
      <li><a href="/about">关于</a></li>
    </ul>
  </nav>
</header>

<main>
  <article>
    <h2>文章标题</h2>
    <p>文章内容...</p>
  </article>
  
  <aside>
    <h3>侧边栏</h3>
    <p>相关内容...</p>
  </aside>
</main>

<footer>
  <p>© 2024 版权所有</p>
</footer>

8.2 语义化标签说明

标签 说明
<header> 页头/文章头部
<nav> 导航栏
<main> 主要内容(页面唯一)
<article> 独立文章内容
<section> 文档中的节
<aside> 侧边栏/附加内容
<footer> 页脚/文章尾部
<figure> 图片、图表等
<figcaption> figure 的标题
<mark> 高亮文本
<time> 时间日期
<address> 联系信息

8.3 使用示例

<article>
  <header>
    <h1>文章标题</h1>
    <time datetime="2024-01-26">2024年1月26日</time>
  </header>
  
  <section>
    <h2>第一章</h2>
    <p>这是第一章的内容,其中<mark>重要部分</mark>被高亮显示。</p>
  </section>
  
  <figure>
    <img src="image.jpg" alt="配图">
    <figcaption>图片说明</figcaption>
  </figure>
  
  <footer>
    <address>
      作者:<a href="mailto:author@example.com">作者邮箱</a>
    </address>
  </footer>
</article>

九、多媒体

9.1 音频

<audio controls>
  <source src="audio.mp3" type="audio/mpeg">
  <source src="audio.ogg" type="audio/ogg">
  您的浏览器不支持音频播放。
</audio>

<!-- 属性 -->
<audio 
  src="audio.mp3" 
  controls 
  autoplay 
  loop 
  muted
  preload="auto">
</audio>

9.2 视频

<video controls width="640" height="360">
  <source src="video.mp4" type="video/mp4">
  <source src="video.webm" type="video/webm">
  您的浏览器不支持视频播放。
</video>

<!-- 属性 -->
<video 
  src="video.mp4" 
  controls 
  autoplay 
  loop 
  muted
  poster="poster.jpg"
  preload="auto">
</video>

9.3 iframe(嵌入内容)

<!-- 嵌入网页 -->
<iframe src="https://example.com" width="800" height="600"></iframe>

<!-- 嵌入视频 -->
<iframe 
  src="https://www.youtube.com/embed/VIDEO_ID" 
  width="560" 
  height="315"
  frameborder="0"
  allowfullscreen>
</iframe>

十、其他常用标签

10.1 分组标签

<!-- div:块级容器 -->
<div class="container">
  <p>内容</p>
</div>

<!-- span:行内容器 -->
<p>这是<span class="highlight">高亮</span>文本</p>

10.2 其他标签

<!-- 地址 -->
<address>
  北京市朝阳区<br>
  电话:010-12345678
</address>

<!-- 缩写 -->
<abbr title="World Wide Web">WWW</abbr>

<!-- 定义 -->
<dfn>HTML</dfn>是超文本标记语言

<!-- 键盘输入 --><kbd>Ctrl</kbd>+<kbd>C</kbd>复制

<!-- 变量 -->
<var>x</var> = 10

<!-- 样本输出 -->
<samp>Hello World</samp>

<!-- 进度条 -->
<progress value="70" max="100">70%</progress>

<!-- 度量 -->
<meter value="0.6">60%</meter>

十一、属性

11.1 全局属性

<!-- id:唯一标识 -->
<div id="header">头部</div>

<!-- class:类名 -->
<div class="container main">容器</div>

<!-- style:内联样式 -->
<p style="color: red;">红色文本</p>

<!-- title:提示信息 -->
<a href="#" title="点击查看详情">链接</a>

<!-- data-*:自定义数据 -->
<div data-user-id="123" data-role="admin">用户信息</div>

<!-- hidden:隐藏元素 -->
<div hidden>隐藏内容</div>

<!-- tabindex:Tab 键顺序 -->
<input type="text" tabindex="1">
<input type="text" tabindex="2">

<!-- contenteditable:可编辑 -->
<div contenteditable="true">可编辑内容</div>

<!-- draggable:可拖拽 -->
<div draggable="true">可拖拽元素</div>

11.2 无障碍属性

<!-- aria-label:标签 -->
<button aria-label="关闭">×</button>

<!-- aria-labelledby:关联标签 -->
<div id="username-label">用户名</div>
<input type="text" aria-labelledby="username-label">

<!-- aria-describedby:描述 -->
<input type="text" aria-describedby="help-text">
<div id="help-text">请输入用户名</div>

<!-- role:角色 -->
<div role="button" tabindex="0">按钮</div>

十二、字符实体

<!-- 常用字符实体 -->
&lt;      <!-- < -->
&gt;      <!-- > -->
&amp;     <!-- & -->
&quot;    <!-- " -->
&apos;    <!-- ' -->
&nbsp;    <!-- 不间断空格 -->
&copy;    <!-- © -->
&reg;     <!-- ® -->
&trade;   <!-- ™ -->
&deg;     <!-- ° -->
&times;   <!-- × -->
&divide;  <!-- ÷ -->
&euro;    <!-- € -->

十三、最佳实践

13.1 代码规范

<!-- ✅ 好的做法 -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>页面标题</title>
</head>
<body>
  <h1>标题</h1>
  <p>段落内容</p>
</body>
</html>

<!-- ❌ 避免 -->
<HTML>
<HEAD>
<TITLE>标题</TITLE>
</HEAD>
<BODY>
<H1>标题</H1>
</BODY>
</HTML>

13.2 语义化

<!-- ✅ 使用语义化标签 -->
<header>
  <nav>
    <ul>
      <li><a href="/">首页</a></li>
    </ul>
  </nav>
</header>

<!-- ❌ 避免过度使用 div -->
<div class="header">
  <div class="nav">
    <div class="nav-item">
      <a href="/">首页</a>
    </div>
  </div>
</div>

13.3 可访问性

<!-- ✅ 提供 alt 属性 -->
<img src="photo.jpg" alt="一张美丽的风景照片">

<!-- ✅ 使用 label 关联表单 -->
<label for="username">用户名:</label>
<input type="text" id="username" name="username">

<!-- ✅ 使用语义化标签 -->
<button type="submit">提交</button>
而不是
<div onclick="submit()">提交</div>

13.4 性能优化

<!-- 图片懒加载 -->
<img src="image.jpg" alt="描述" loading="lazy">

<!-- 预加载关键资源 -->
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>

<!-- DNS 预解析 -->
<link rel="dns-prefetch" href="https://cdn.example.com">

<!-- 预连接 -->
<link rel="preconnect" href="https://api.example.com">

十四、HTML5 新特性

14.1 新的输入类型

<input type="email">
<input type="url">
<input type="tel">
<input type="search">
<input type="range">
<input type="date">
<input type="time">
<input type="datetime-local">
<input type="month">
<input type="week">
<input type="color">

14.2 新的表单属性

<!-- placeholder:占位符 -->
<input type="text" placeholder="请输入">

<!-- autofocus:自动聚焦 -->
<input type="text" autofocus>

<!-- required:必填 -->
<input type="text" required>

<!-- pattern:正则验证 -->
<input type="text" pattern="[0-9]{11}">

<!-- list:数据列表 -->
<input type="text" list="cities">
<datalist id="cities">
  <option value="北京">
  <option value="上海">
  <option value="广州">
</datalist>

14.3 Canvas 和 SVG

<!-- Canvas:位图 -->
<canvas id="myCanvas" width="200" height="200"></canvas>
<script>
  const canvas = document.getElementById('myCanvas');
  const ctx = canvas.getContext('2d');
  ctx.fillStyle = 'red';
  ctx.fillRect(10, 10, 50, 50);
</script>

<!-- SVG:矢量图 -->
<svg width="200" height="200">
  <circle cx="100" cy="100" r="50" fill="blue"/>
</svg>

十五、调试技巧

15.1 验证 HTML

  • 使用 W3C 验证器:validator.w3.org/
  • 浏览器开发者工具检查
  • 检查语义化是否正确

15.2 常见问题

问题 原因 解决
页面显示乱码 字符编码未设置 添加 <meta charset="UTF-8">
样式不生效 选择器错误 检查 class/id 拼写
表单不提交 action/method 错误 检查表单属性
图片不显示 路径错误 检查 src 路径

十六、推荐资源

官方文档:

学习资源:


十七、总结

HTML 核心要点:

语义化标签 + 表单验证 + 可访问性 + 性能优化 = 高质量网页

核心心法:

HTML 是网页的骨架,语义化是 HTML 的灵魂。 结构清晰、语义明确、易于维护。


📝 文档信息

  • 作者: 阿鑫
  • 更新日期: 2026.1.26

vue3开发容易忽略的地方

2026年1月26日 16:06

工作了很多年,使用vue3也有3年了,但是一直像一个螺丝钉一样复制粘贴,没有学习什么东西,能熟练梳理业务逻辑,但是思路和方法跟用vue2没有什么区别。决定重新刷一遍文档,把没用过的,不太熟悉的地方罗列出来,希望能在开发中多多使用。

ref在模板中解包

  • 在模板渲染上下文中,只有顶级ref才会被解包.
const count = ref(1);
const obj = ref({id: ref(1)}) // obj.id 在模板中不能被解包

{{obj.id + 1}} // 输出错误:[object object]1
{{obj.id}}     // 输出正确:1
  • {{ obj.id }} 与 {{ obj.id.value }}等价,前者是文本插值的便利特性。

reactive

-不推荐使用 reactive() 的泛型参数,因为处理了深层次 ref 解包的返回值与泛型参数的类型不同。 -对解构操作不友好,我们解构reative时,解构后的变量与原变量失去连接。

// 不使用reative泛型参数
const book:Book = reative({name: 'xxx'})

let {count} = reativeDemo;
count++;  // 此时,reativeDemo.count不会发生变化。

computed

  • computed属性会基于响应式依赖缓存:一个computed属性只会在他的响应式依赖更新时重新计算。
  • computed可以获取上一次的值computed((pre) => {return count.value > 0 ? count.value : pre})
  • 可以重写computed的get和set方法,但是不建议。

v-show/v-if

  • v-show不支持在template上使用(v-if可以)
  • v-if有惰性,一开始为false则不会渲染,直到为true才第一次渲染

v-for

  • 循环使用item in item或者item of items一样
  • (value, key, index) in obj
  • v-for与v-if不建议同时使用,v-if优先级更高,所以v-if中不能使用item的值

函数ts类型

function handleChange(event: Event) { 
    console.log((event.target as HTMLInputElement).value) 
}

watch/watchEffect

  • deep属性可以为数字,表示监听的深度。
  • once属性,只监听一次。
  • watchEffect: 用法类似computed,不需要指定监听对象。在同步执行期间,自动追踪所有能访问到的响应式数据,在异步执行期间,只有在第一个await正常工作之前的响应式数据能被追踪。
  • onWatcherCleanup: 注册一个清理函数,当监听器失效并准备重新运行时调用(eg:在watch中id变化调用接口,接口未返回数据时,id又发生了变化,则需要取消正在进行的接口调用,根据新的id重新发起接口)(3.5+)
  • watch方法会在dom更新之前调用,如果想在dom更新之后调用则需要增加属性{flush: 'post'},watch方法在任何更新之前调用,则使用{flush: 'sync}(3.5+)
watch(source, callback, {flush: 'post'})
watchEffect(callback, {flush: 'post'})
watchPostEffect(callback)

// 在响应数据变化时同步执行
watch(source, callback, {flush: 'sync'})
watchEffect(callback, {flush: 'sync'})
watchSyncEffect(callback)
  • 中止监听器,定义一个变量接受watch或者watchEffect的返回值
const unwatch = watch(source, callback)
unwatch() // 中止监听

模板引用useTemplateRef(3.5+)

  • 会自动推断ref的数据类型
  • 如果自动推断失败可以自定义类型,使用InstanceType(typeof MyComponent) / HTMLInputElement
const inputRef = useTemplateRef('my-input');
inputRef.focus();
  • v-for上使用ref,则useTemplateRef生成的是一个数组,数组的顺序不一定与源数组顺序相同。
<div ref="itemsRef" v-for="item in items" >{{ item }}</div>

const itemsRef = useTemplateRef('itemsRef')

props

  • 解构props时,对象数组等不需要在函数中返回。(3.5+)
  • 给props赋值的时候使用definedProps<>()
  • 定义props分编译时和运行时,运行时可以验证一些复杂数据类型以及定义动态数据。编译时可以使用泛型等复杂数据结构,ts中更推荐使用编译时。
  • 给组件传递一个对象的所有属性可以使用v-bind写法
  • 子组件不能修改父组件传递的props,可以重新定义一个变量。
  • boolean类型的props,为了更贴近原生html,有简介写法。
// 解构props时,对象数组等不需要在函数中返回。(3.5+)
const {name: '', age: 0, hobby: []} = defineProps<{name: string, age: number, hobby: string[]}>();

// 3.5以下版本 
// withDefaults中设置默认值时,对象数组等需要在函数中返回,确保多个实例时,每个实例都有自己的副本。
const props = widthDefaults(defineProps<{name: string, age: number, hobby: string[]}>(), {
name: '',
age: 0,
hobby: () => []
})

// 运行时
const props = defineProps({
name: {
    type: String,
    default: '',
    validate: (val) => {...}
}
})

// 编译时
const defineProps<{name: string, age: number, hobby: string[]}>

// boolean类型的props
const props = defineProps({disabled: Boolean})
<myComponent disabled /> // 相当于 :disabled="true",不写默认为false

emit

  • 可以带校验,校验通过再emit,否则不emit
  • 更简洁的定义语法(3.3+)
// 校验
const emits = defineEmits({
    submit: (name: string, id: number) => {
        if (name && id) { return true }
        else { return false;}
    }
})


const emits = defineEmits<{
    update: [id: number],
    success: [],
}>()

// v-bind
const post = {name: '', id: 0}
<myComponent v-bind="post" />
<myComponent :id="post.id" :name="post.name" />

defineModel

  • 定义了一个名为modelValue的prop,本地的ref与其同步
  • 定义了一个update:modelValue方法,本地ref变化时触发(.set方法中实现)
// 第一个参数不写默认与ref名相同
const modelValue = defineModel('value', {require: truedefault0})

attribute透传

  • 组件接收的所有未在props中声明的属性会透传
  • 多根节点默认不会透传,除非显示透传,使用$attrs
  • 可以通过vue提供的方法获取所有透传的attrs,attrs不是响应式的,需要响应式的需要props定义。
import { useAttrs } from 'vue'
const attrs = useAttrs();
  • 想要父组件的属性透传给自己的子组件可以使用$attrs, attrs对象包含了除组件所声明的 props 和 emits 之外的所有其他 attribute,例如 classstylev-on 监听器等等。
  • 想要不透传,或者不接受透传可以设置
<childComponent v-bind="$attrs">

// 不透传设置
<scripte setup>
defineOptions({
    inheritAttrs: false
})
</script>

slot

  • 组件插槽可以传递参数,参数名不能为name,name为插槽名。
// Child
<div class="card">
    <slot name="cardContent" value="cardInfo">
</div>
// Parent
<template #cardContent="cardInfo">
    {{cardInfo}}
</template>

依赖注入

  • 可以inject一个ref的响应式数据,数据变化会同步。在子孙组件修改会同步父组件,在父组件修改会同步到子组件。但是建议所有的更改都在父组件中执行,可以通过传递一个update方法给子孙组件更新。
import { readonly } from 'vue'
// 父组件
const location = ref('');
function updateLocation() {
    location.value = "ww"
}
privide('location', {
    location,
    updateLocation,
})

// 子孙组件
const {location, updateLocation} = inject('location')
  • 不想被子孙组件更改的数据使用readonly方法包裹
privide('readonlyLocation', readonly(location))
  • vue推荐当依赖注入较多时或写公用组件,使用Symbol防止重名,将所有Symbol定义的key放到一个文件中
// key.js
const locationkey  = Symbol();
// parent
import { locationkey } from './key.js'
privide(locationKey, {...})
// child
import { locationkey } from './key.js'
const location = inject(locationkey);

组合式函数

  • 方法中使用了vue的组合式API(如ref,watch等)来封装和复用的有状态逻辑的函数。
  • 约定
    • 通常使用useXxx命名
    • 返回的数据一般都为ref,这样在组件中被解构后仍为响应式数据。
    const x = ref(1);
    const y = ref(2);
    return {
        x, y
    }
    
  • 在不同组件中使用useXxx都会重新创建新的数据,各个数据互不影响。

自定义指令

  • 由一个包含类似组件生命周期钩子的对象来定义。在ts setup中,v开头的方法都可以直接作为指令使用
  • 指令不建议在组件上使用,组件可能有多个子组件,指令不会通过$attrs透传。
<script setup>
const vHighlight = {
    mounted: (el) => {
        el.classList.add('high-light')
    }
}
</script>
<template>
<div v-highlight>aaaaaaaa</div>
</template>

vue官方推荐

  • 使用vscode编辑器
  • vue official:vscode插件,提供实时语言服务
  • 浏览器开发者插件:Vue DevTools
  • vue-tsc: 在编译阶段进行类型检查。
  • 代码规范:在项目中安装eslint-plugin-vue; 在IDE中装ESlint插件;在项目中装lint-staged代码提交规范
  • 格式化:prettier,IDE中安装插件,项目中安装prettier,写代码格式化时IDE中的prettier会优先找项目中的prettier配置进行格式化,如果项目中没有则会使用IDE自己的配置。
  • @vitejs/plugin-vue:为 Vite 提供 Vue 单文件组件支持的官方插件。

状态管理

  • 用响应式API(ref,reative, watch等)做状态管理。
  • 定义一个store文件,将多个组件需要公用的数据放进去,在组件中引用
  • 复杂的使用pinia

vue3+ts+eslint+prettier 实现代码风格统一

2026年1月26日 16:03

vue3+ts+eslint+prettier 实现代码风格统一

一.安装eslint

npm i eslint -D

1.初始化eslint

当前文章是基于 eslint9.39.2 版本创作

创建eslint配置文件,eslint8.21.0之后可以使用eslint.config.js文件名。

或使用命令初始化eslint配置文件

npx eslint --init

1)安装@eslint/config@lates image.png 2)选择要检测的文件 image.png 3)选择如何使用eslint image.png 4)选择使用哪种模块类型 image.png 5)框架选择 image.png 6)是否使用ts image.png 7)选择运行环境 image.png 8)希望配置文件用什么语法 image.png 9)选择要安装解析相关依赖包 image.png 10)选择希望使用的安装包工具 image.png

2.修改eslint.config.js中的相关配置
import pluginJs from '@eslint/js';
import tseslint from 'typescript-eslint';
import pluginVue from 'eslint-plugin-vue';
import prettier from 'eslint-plugin-prettier';
import prettierConfig from 'eslint-config-prettier';
import eslintparser from 'vue-eslint-parser';
export default [
  {
    files: ['**/*.{js,mjs,cjs,vue}'],
    ignores: [
        'node_modules/**', 
        'dist/**', 
        'public/**', 
        'coverage/**', 
        'src/assets/**'
    ],
    languageOptions: {
      globals: globals.browser,
      parser: eslintparser,
      parserOptions: {
        ecmaVersion: 2020,
        sourceType: 'module',
        parser: '@typescript-eslint/parser',
        ecmaFeatures: {
          jsx: true,
        },
      },
    },
  },

  pluginJs.configs.recommended,
  ...tseslint.configs.recommended,
  ...pluginVue.configs['flat/essential'],
  ...pluginVue.configs['flat/recommended'],
  prettierConfig, // 解决prettier和eslint的冲突
  {
    plugins: {
      prettier: prettier, // 使用prettier格式化
    },
    rules: {
      // 开启这条规则后,会将prettier的校验规则传递给eslint,这样eslint就可以按照prettier的方式来进行代码格式的校验
      'prettier/prettier': 'error',
      'no-var': 'error', // 要求使用 let 或 const 而不是 var
      'no-console': 'off',
      'no-restricted-globals': 'off',
      'no-restricted-syntax': 'off',
      'vue/multi-word-component-names': 'off', // 禁用vue文件强制多个单词命名
      'no-multiple-empty-lines': ['warn', { max: 1 }],
      'vue/valid-template-root': 'off',
      'no-unused-vars': 'off',
      'no-undef': 'off', // 自动引入vue和UI组件库报错问题
      '@typescript-eslint/no-unused-vars': 'off', // 关闭未使用变量规则
      '@typescript-eslint/no-explicit-any': 'off', // 关闭ts中any校验
      semi: ['error', 'always'],
      'no-debugger': 'warn', //提交时不允许有debugger
      'no-console': [
        //提交时不允许有console.log
        'warn',
        {
          allow: ['warn', 'error'],
        },
      ],
    },
  },
];

3.修改vite.config.ts
// 引入vite-plugin-eslint 
import eslintPlugin from 'vite-plugin-eslint' 
// 配置plugins 
eslintPlugin({ 
    include: ['src/**/*.js', 'src/**/*.vue'], 
    cache: true, 
}),

4.命令行式运行:修改 package.json
{ 
... 
"scripts": { 
    ... 
    "eslint:comment": "使用 ESLint 检查并自动修复 src 目录下所有扩展名为 .js 和 .vue 的文件", 
    "eslint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src",//旧版 
    "eslint": "eslint \"src/**/*.{js,ts,vue}\" --fix",//新版 } 
... 
}
5.注意

eslintignore文件已经废除,新版本的gnore写在eslint.config.js中

image.png

二.安装prettier

1.安装

npm i prettier -D

npm i eslint-config-prettier -D

cnpm i eslint-plugin-prettier -D

2.创建配置文件: .prettierrc.js
// 一行最多 120 字符 
printWidth: 120, 
// 使用 2 个空格缩进
tabWidth: 2, 
// 不使用 tab 缩进,而使用空格 
useTabs: false, 
// 行尾需要有分号 
semi: true, 
// 使用单引号代替双引号 
singleQuote: true,
// 对象的 key 仅在必要时用引号 
quoteProps: 'as-needed', 
// jsx 不使用单引号,而使用双引号 
jsxSingleQuote: false, 
// 末尾使用逗号 
trailingComma: 'all', 
// 大括号内的首尾需要空格{ foo: bar }
bracketSpacing: true, 
// jsx 标签的反尖括号需要换行 
jsxBracketSameLine: false, 
// 箭头函数,只有一个参数的时候,也需要括号 
arrowParens: 'always', 
// 每个文件格式化的范围是文件的全部内容 
rangeStart: 0, 
rangeEnd: Infinity, 
// 不需要写文件开头的 
@prettier requirePragma: false, 
// 不需要自动在文件开头插入 
@prettier insertPragma: false, 
// 使用默认的折行标准 
proseWrap: 'preserve', 
// 根据显示样式决定 html 要不要折行 
htmlWhitespaceSensitivity: 'css', 
// 换行符使用 lf endOfLine: 'auto' 
}
3.修改 eslint.config.js 配置,添加prettier
import prettier from 'eslint-plugin-prettier' 
export default [ 
        ...
        { 
        plugins: {
            prettier: prettier,
        }, 
        rules: { 
            'prettier/prettier': 'error', 
        ... 
        }, 
    }, 
]
4.命令行式运行:修改 package.json
{ 
    ... 
    "scripts": { 
        ... 
        "prettier:comment": "自动格式化当前目录下的所有文件", 
        "prettier": "prettier --write" 
        } 
    ... 
}
5.解决eslint和pretter中的冲突
// eslint.config.js 
import prettier from 'eslint-plugin-prettier' 
import prettierConfig from'eslint-config-prettier'; 
export default [ 
    .... 
    // 插件的默认推荐配置需要直接包含在配置数组中 
    prettierConfig, 
    { 
        plugins: { 
            prettier: prettier, //解决prettier和eslint的冲突 
        }, 
        rules: { 
            'prettier/prettier': 'error', 
            ... 
        }, 
    }, 
]

三.创建.vscode文件夹

在项目的根目录创建文件夹.vscode,再在目录中创建settings.json文件。将如下代码拷其中

{ 
"editor.formatOnSave": true, // 核心:保存时自动格式化 
"editor.quickSuggestions": true,// 启用代码提示
"eslint.enable": true, //启用 ESLint 插件 
//ESLint 应该验证哪些文件类型 
"eslint.validate": [
    "javascript", 
    "javascriptreact", 
    "vue-html", 
    "typescript", 
    "html", 
    "vue"
 ], 
"eslint.options": { 
    // 检查这些文件类型
    "extensions": [".js", ".jsx", ".ts", ".tsx", ".vue"] 
 },
// 可选:保存时先修复代码问题(如ESLint报错)再格式化(前端常用) 
"editor.codeActionsOnSave": { 
    "source.fixAll": true
}, 
// 可选:指定默认格式化工具(比如前端优先用Prettier) 
"editor.defaultFormatter": "esbenp.prettier-vscode" ,
"[vue]": { 
    "editor.defaultFormatter": "esbenp.prettier-vscode"
    }, 
"[typescript]": { 
    "editor.defaultFormatter": "esbenp.prettier-vscode" 
    }
}

Windows 开机自启动的 8 种姿势:写给开发者的深度对比指南

作者 BanShan_Alec
2026年1月26日 16:02

Windows 开机自启动的 8 种姿势:写给开发者的深度对比指南

最近在做桌面应用的开机自启动功能,踩了不少坑。今天就来聊聊 Windows 系统下那些五花八门的自启动方式,帮你找到最适合你项目的那一款。

📦 本文所有测试数据均来自开源项目windows-startup-run-demo,包含完整的测试代码、配置脚本和原始日志,欢迎 Star ⭐ 和复现验证!

🎭 先说结论

如果你赶时间,直接看这个表格:

方式 启动时机 实测延迟 接入成本 杀软友好 多用户 推荐场景
Windows 服务 系统启动 ~13s ⭐⭐⭐ 后台服务、VPN、代理
任务计划(系统启动) 系统启动后 ~14s ⭐⭐⭐ 需要早启动的用户程序
Userinit 追加 用户登录时 ~13s ⭐⭐ ⚠️ 不推荐
任务计划(用户登录) 用户登录 ~19s ⭐⭐⭐ 最佳平衡方案
注册表 HKLM\Run 用户登录 ~52s ⭐⭐ 多用户共享的轻量程序
注册表 HKCU\Run 用户登录 ~79s 最简单的用户级自启
启动文件夹 用户登录后 ~81s 小白用户自定义
Shell 替换 用户登录时 - ⭐⭐⭐⭐ 千万别用

💡 延迟数据来自实际测试(SSD 机器,从开机到程序执行的时间)—— 测试代码开源在 GitHub


🚀 详细拆解每种方式

1. Windows 服务 (Service) —— 最早起床的那个

启动延迟: ~13秒 | 权限: 管理员 | 复杂度: 中等

Windows 服务是系统级的进程,在用户登录之前就已经跑起来了。

优点:

  • 🏃 启动最早,系统刚起来就开始工作
  • 🛡️ 杀软完全不会拦截(正规服务)
  • 👥 天然支持多用户,跟用户会话无关
  • 🔄 可配置自动重启、依赖关系等

缺点:

  • 🖥️ 没有 GUI 能力(Session 0 隔离问题)
  • 📦 需要额外工具来包装(如 WinSW、NSSM)
  • ⚙️ 安装需要管理员权限

适合场景: VPN 客户端、系统代理(Clash)、后台同步服务

代码示例(使用 schtasks 创建):

# 或者用 sc.exe 创建原生服务
sc.exe create "MyService" binPath= "C:\path\to\service.exe" start= auto

实战经验: 如果你是 Node.js/Electron 开发者,强烈推荐 WinSW,只需要一个 XML 配置文件就能把普通 exe 包装成服务。


2. 任务计划 - 系统启动触发 (Task Scheduler - At Startup)

启动延迟: ~14秒 | 权限: 管理员 | 复杂度: 中等

这是通过 Windows 任务计划程序,设置"计算机启动时"触发的任务。

优点:

  • ⚡ 启动很早,仅次于服务
  • 🎛️ 可以精细控制(延迟启动、条件触发、优先级等)
  • 🖥️ 可以有 GUI(跟服务最大的区别!)
  • 📋 系统原生支持,杀软友好

缺点:

  • 📝 配置相对复杂(XML 或 COM API)
  • 🔑 需要管理员权限创建

适合场景: 需要早启动 + 有界面的应用

代码示例(schtasks 命令):

schtasks /create /tn "MyApp-Startup" /tr "C:\path\to\app.exe" /sc onstart /ru SYSTEM

Pro Tip: 你可以设置任务的 Priority0(实时)到 10(空闲),默认是 7。想启动更快?调低这个值!


3. Userinit 追加 —— 危险的捷径 ⚠️

启动延迟: ~13秒 | 权限: 管理员 | 复杂度: 低 | 风险: 高

Userinit 是 Windows 登录过程的一部分,通过修改注册表:

HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\Userinit

默认值是 C:\Windows\system32\userinit.exe,,你可以在后面追加自己的程序。

优点:

  • ⚡ 启动非常早,几乎和服务同时

缺点:

  • 🚨 极易被杀软报毒! 这是恶意软件常用的驻留方式
  • 💥 配置错误会导致无法登录系统
  • ❌ 不支持多用户(只能全局配置)
  • 🔧 只能用于单个程序的追加

我的建议: 除非你在做安全研究,否则 千万别用。就算用,也要做好被 360、火绒拦截的心理准备。


4. 任务计划 - 用户登录触发 (Task Scheduler - At Logon) ⭐ 推荐

启动延迟: ~19秒 | 权限: 普通/管理员 | 复杂度: 中等

这是我个人最推荐的方式,平衡了启动速度、安全性和灵活性。

优点:

  • 📈 启动较早(比注册表 Run 快得多!)
  • 🔐 可以不需要管理员权限(只为当前用户创建)
  • 🛡️ 杀软完全不会拦截
  • 👥 天然支持多用户(每个用户独立任务)
  • 🎛️ 可精细控制(延迟、条件、重试策略等)
  • 🖥️ 支持 GUI 程序

缺点:

  • 📝 需要处理 XML 或 schtasks 命令
  • 🔍 调试稍微麻烦一点

代码示例:

# 为当前用户创建登录触发的任务
schtasks /create /tn "MyApp-Logon" /tr "C:\path\to\app.exe" /sc onlogon

用 XML 定义(更灵活):

<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
  <Triggers>
    <LogonTrigger>
      <Enabled>true</Enabled>
    </LogonTrigger>
  </Triggers>
  <Principals>
    <Principal>
      <LogonType>InteractiveToken</LogonType>
      <RunLevel>LeastPrivilege</RunLevel>
    </Principal>
  </Principals>
  <Settings>
    <Priority>4</Priority>
    <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
  </Settings>
  <Actions>
    <Exec>
      <Command>C:\path\to\app.exe</Command>
    </Exec>
  </Actions>
</Task>

5. 注册表 HKLM\Run —— 老牌稳定选手

启动延迟: ~52秒 | 权限: 管理员 | 复杂度: 低

位置:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run

这是最经典的自启动方式之一,所有用户登录时都会执行。

优点:

  • 📖 实现简单,一行注册表搞定
  • 👥 支持多用户(所有用户共享)
  • 🛡️ 杀软友好(正规做法)

缺点:

  • 🐢 启动较晚(等用户登录流程走完)
  • 🔑 需要管理员权限

代码示例:

# PowerShell
Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" -Name "MyApp" -Value "C:\path\to\app.exe"
// Node.js (使用 regedit 包)
const regedit = require('regedit');
regedit.putValue({
  'HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run': {
    'MyApp': { value: 'C:\\path\\to\\app.exe', type: 'REG_SZ' }
  }
});

6. 注册表 HKCU\Run —— 最简单的用户级方案

启动延迟: ~79秒 | 权限: 普通 | 复杂度: 最低

位置:HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Run

优点:

  • 不需要管理员权限!
  • 📖 实现最简单
  • 🛡️ 杀软友好

缺点:

  • 🐢 启动很晚
  • ❌ 不支持多用户(只对当前用户生效)

适合场景: 不需要快速启动的普通用户应用(如 微信)

代码示例:

// Electron 内置支持!
const { app } = require('electron');

// 开启自启动
app.setLoginItemSettings({
  openAtLogin: true,
  path: app.getPath('exe')
});

🎯 Electron 的 setLoginItemSettings 默认就是写 HKCU\Run,简单粗暴。


7. 启动文件夹 (Startup Folder) —— 小白最爱

启动延迟: ~81秒 | 权限: 普通 | 复杂度: 最低

位置:

  • 当前用户:%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup
  • 所有用户:C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup

优点:

  • 👀 用户可见,透明可控
  • 🔧 用户可以自己管理(拖拽快捷方式即可)
  • 🛡️ 杀软绝对不会拦截

缺点:

  • 🐢 启动最晚(所有方式中最慢)
  • 🗂️ 容易被用户误删

适合场景: 让用户自己决定是否开机启动的情况

代码示例:

const fs = require('fs');
const path = require('path');
const { shell } = require('electron');

// 获取启动文件夹路径
const startupFolder = path.join(
  process.env.APPDATA,
  'Microsoft/Windows/Start Menu/Programs/Startup'
);

// 创建快捷方式
shell.writeShortcutLink(
  path.join(startupFolder, 'MyApp.lnk'),
  { target: process.execPath }
);

8. Shell 替换 —— 别碰它 ☠️

权限: 管理员 | 风险: 极高

位置:HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\Shell

这个注册表键决定了用户登录后启动什么程序(默认是 explorer.exe)。

理论上你可以:

explorer.exe, C:\path\to\your\app.exe

但是:

  • 🚨 大概率被杀软直接干掉
  • 💀 配置错误 = 系统无法进入桌面
  • 🦠 这是恶意软件最爱用的手法

我的态度: 如果有人让你用这个方法,拉黑他。


📊 启动时间真实日志

开机时间线 (SSD 机器实测)
─────────────────────────────────────────────────────────────────────────
2026-01-26 14:59:33.67 | SERVICE         | Boot: 2026-01-26 14:59:20.50 | +13.18s
2026-01-26 14:59:33.71 | USERINIT        | Boot: 2026-01-26 14:59:20.50 | +13.21s
2026-01-26 14:59:34.14 | TASK_STARTUP    | Boot: 2026-01-26 14:59:20.50 | +13.65s
2026-01-26 14:59:39.01 | TASK_LOGON      | Boot: 2026-01-26 14:59:20.50 | +18.52s
2026-01-26 15:00:12.94 | HKLM_RUN        | Boot: 2026-01-26 14:59:20.50 | +52.44s
2026-01-26 15:00:39.34 | HKCU_RUN        | Boot: 2026-01-26 14:59:20.50 | +78.84s
2026-01-26 15:00:41.96 | STARTUP_FOLDER  | Boot: 2026-01-26 14:59:20.50 | +81.47s

🔒 安全性对比:杀软怎么看?

方式 360 火绒 Windows Defender 说明
Windows 服务 ⚠️ ⚠️ 可能被标记为可疑
任务计划 系统原生功能
HKLM\Run ⚠️ ⚠️ 标准做法
HKCU\Run ⚠️ ⚠️ 标准做法
启动文件夹 用户可见,最安全
Userinit ⚠️ ⚠️ ⚠️ 高概率被标记为可疑
Shell 替换 ⚠️ 高概率被拦截

🎯 不同场景的选择建议

场景1:普通桌面应用(QQ/微信类)

推荐: HKCU\Run 或 Electron 的 setLoginItemSettings

  • 不需要管理员权限
  • 接入成本最低
  • 启动晚点无所谓

场景2:需要尽早启动的应用(VPN/代理类)

推荐: 任务计划(用户登录触发)+ 低优先级

  • 比注册表快 60 秒
  • 不需要管理员权限也能配置
  • 杀软友好

场景3:纯后台服务(同步/监控类)

推荐: Windows 服务

  • 最早启动
  • 自动重启能力
  • 但需要 WinSW 等工具包装

场景4:多用户环境

推荐: 任务计划 或 HKLM\Run

  • 需要管理员权限
  • 所有用户共享配置

📝 总结

  1. 追求速度 → 任务计划(系统启动触发)或 Windows 服务
  2. 追求简单 → HKCU\Run(Electron 原生支持)
  3. 追求稳妥 → 任务计划(用户登录触发)⭐ 推荐
  4. 追求透明 → 启动文件夹
  5. 追求刺激 → Userinit / Shell 替换(开玩笑的,别用)

希望这篇文章能帮你在做开机自启动功能时少踩坑。如果你有其他问题或者发现了更好的方案,欢迎交流!


🔗 相关资源

测试项目开源地址: windows-startup-run-demo

项目包含:

  • ✅ 完整的 8 种自启动方式配置脚本
  • ✅ 可复现的启动时间测试程序
  • ✅ 原始测试日志数据
  • ✅ 一键配置 / 清理脚本

欢迎 Clone 下来在自己机器上跑一遍,看看你的环境下各种方式的启动时间差异!


参考文档

React Scheduler 最小堆实现

作者 _DoubleL
2026年1月26日 15:57

1. 什么是最小堆

最小堆(Min Heap)是一种完全二叉树结构,同时满足一个很关键的性质:任意一个节点的值,都 ≤ 它的左右子节点的值,也就是说: 👉 堆顶(根节点)一定是整个结构中最小的元素

完全二叉树的特点是:

  • 从上到下
  • 从左到右依次填满
  • 只允许最后一层不满 image.png

2. 用数组表示最小堆🔥

最小堆通常用数组实现,而不是链表。

索引:  0  1  2  3  4  5
数组: [1, 3, 5, 4, 6, 8]

如果数组下标从 0 开始:

  • 父节点:(i - 1) >>> 1;
  • 左子节点:2 * i + 1
  • 右子节点:2 * i + 2

3. React Scheduler 最小堆实现

React 的 Scheduler 内部,正是通过 最小堆 + sortIndex 来维护任务执行顺序

export type Node = {
  id: number;        // 每个任务的唯一标识
  sortIndex: number; // 决定任务顺序
};

堆的本质:数组

export type Heap<T extends Node> = Array<T>;

3.1 取出堆顶元素

export const peek = <T extends Node>(heap: Heap<T>): T | null => {
  return heap.length === 0 ? null : heap[0];
};

3.2 插入元素

插入流程:

  1. 新元素放到数组末尾
  2. 从下往上进行 堆化(siftUp),不断与父节点比较,若比父节点小就交换,一直向上,直到满足堆性质
  3. 恢复最小堆性质

如下图,在最后的节点插入1, 1和父节点(10)交换位置, 接着 1和父节点(2) 交换位置,就变成了恢复了最小堆

image.png

// 插入元素
export const push = <T extends Node>(heap: Heap<T>, node: T): void => {
  // 1. 把node放到堆的最后
  const index = heap.length;
  heap.push(node);
  // 2. 调整最小堆,从下往上堆化
  siftUp(heap, node, index);
};

// 从下往上堆化
export const siftUp = <T extends Node>(
  heap: Heap<T>,
  node: T,
  i: number,
): void => {
  let index = i;

  while (index > 0) {
    // 无符号右移,相当于 /2 并且向下取整
    const parentIndex = (index - 1) >>> 1;
    const parent = heap[parentIndex];
    // 如果父节点大于node,需要交换
    if (compare(parent, node) > 0) {
      // node子节点更小,和根节点交换
      heap[parentIndex] = node;
      heap[index] = parent;
      index = parentIndex;
    } else {
      return;
    }
  }
};

// 比较函数,返回值大于0 表示 a大于b
function compare(a: Node, b: Node) {
  const diff = a.sortIndex - b.sortIndex;
  return diff !== 0 ? diff : a.id - b.id;
}

3.3 删除堆顶元素

删除流程

  1. 保存堆顶元素(最小值)
  2. 取出最后一个元素
  3. 放到堆顶
  4. 从上往下堆化(siftDown),比较 node 与左右子节点,选择 更小的那个子节点,若子节点更小,则交换,一路向下,直到恢复堆序
// 删除堆顶元素  先取出堆顶元素,然后取出最后一个元素放到堆顶,然后从上往下堆化
export const pop = <T extends Node>(heap: Heap<T>): T | null => {
  if (!heap.length) return null;
  const first = heap[0];
  const last = heap.pop()!;
  if (first !== last) {
    // 证明heap中有2个或者更多个元素
    heap[0] = last;
    siftDown(heap, last, 0);
  }
  return first;
};

// 从上往下堆化
function siftDown<T extends Node>(heap: Heap<T>, node: T, i: number): void {
  let index = i;
  const length = heap.length;
  // 只需要取一半的节点,因为每次都是跟左半边 或者 右半边的节点对比
  const halfLength = length >>> 1;
  while (index < halfLength) {
    const leftIndex = (index + 1) * 2 - 1;
    const left = heap[leftIndex];
    const rightIndex = leftIndex + 1;
    const right = heap[rightIndex]; // right不一定存在,等下还要判断是否存在
    if (compare(left, node) < 0) {
      // left<node
      if (rightIndex < length && compare(right, left) < 0) {
        // right存在,且right<left
        heap[index] = right;
        heap[rightIndex] = node;
        index = rightIndex;
      } else {
        // left更小或者right不存在
        heap[index] = left;
        heap[leftIndex] = node;
        index = leftIndex;
      }
    } else if (rightIndex < length && compare(right, node) < 0) {
      // left>=node && right<node
      heap[index] = right;
      heap[rightIndex] = node;
      index = rightIndex;
    } else {
      // 根节点最小,不需要调整
      return;
    }
  }
}

// 比较函数,返回值大于0 表示 a大于b
function compare(a: Node, b: Node) {
  const diff = a.sortIndex - b.sortIndex;
  return diff !== 0 ? diff : a.id - b.id;
}

前端图片自动化压缩:一个支持 CLI 与 Vite 构建集成的无损优化工具

2026年1月26日 15:53

在日常前端开发中,图片资源往往是网页体积的“大头”。之前开发的太阳系仿真系统(代码已开源:gitee.com/zach2019/so… TinyPNG、Squoosh),但它们通常依赖手动上传 → 下载 → 替换的操作流程——不仅重复枯燥,还难以融入现代前端工程化的自动化流水线。 作为开发者,我自然是希望能自动绝不手动,代码能解决的问题绝不麻烦别人。无论是处理存量静态资源,还是在构建时自动压缩被引用的图片,都应无需人工干预。正因如此,我开发了开源工具 @zhaoshijun/compress —— 一套集 CLI 批量处理与 Vite 构建集成于一体的图片自动化压缩方案,真正做到解放前端开发的双手。

npm包地址:www.npmjs.com/package/@zh…

源码地址:gitee.com/zach2019/im…

✨ 核心亮点

  • 「🛠 双模驱动」

    • 「CLI 模式」:一键扫描并批量压缩本地存量图片。
    • 「Vite 插件模式」:在 vite build 构建时自动拦截并压缩引用的图片,开发环境零感知。
  • 「⚡️ 极速性能」:底层基于 Node.js 高性能图片处理库 「Sharp」,速度飞快,质量无损。

  • 「📦 智能缓存」:Vite 插件内置 Content Hash 缓存机制,「未修改的图片跳过压缩」,拒绝拖慢构建速度。

  • 「🛡 安全可靠」:CLI 模式默认不覆盖原图,支持备份;Vite 模式仅处理引用资源。


💡 设计思路解密

这个工具的设计初衷是**“一套逻辑,多端复用”**。将核心压缩能力封装,分别通过 CLI 和 Vite 插件暴露给用户。

1. 核心逻辑:Sharp 封装

为了保证压缩质量和速度,我选择了 sharp。核心函数抹平了不同格式的参数差异。

// src/core/compressor.js
import sharp from 'sharp';

export async function compressImage(input, options) {
  let instance = sharp(input);
  
  // 统一处理格式参数
  if (format === 'png') {
    // 开启调色板量化,显著减小体积
    instance = instance.png({ palettetruecompressionLevel9 });
  } else {
    // JPEG/WebP 默认高质量压缩
    instance = instance.jpeg({ quality: options.quality || 88 });
  }

  return instance.toBuffer();
}

2. Vite 插件:构建流拦截

Vite 插件的核心在于利用 Rollup 的 generateBundle 钩子。在这个阶段,我们能获取到所有即将输出的资源文件,直接替换其内容。

// vite/index.js
export function compressVitePlugin(options) {
  return {
    name'@zhaoshijun/compress',
    apply'build'// 仅在构建时生效
    async generateBundle(_, bundle) {
      for (const fileName in bundle) {
        // 筛选图片资源
        if (isSupportedImage(fileName)) {
          const originalBuffer = bundle[fileName].source;
          
          // ⚡️ 核心:压缩并替换资源内容
          const compressed = await compressImage(originalBuffer, config);
          bundle[fileName].source = compressed;
          
          // 同时更新文件名(如 logo.png -> logo.min.png)
          // 并自动替换 JS/CSS 中的引用路径
        }
      }
    }
  };
}

3. 性能优化:基于内容的哈希缓存

为了避免每次构建都重新压缩所有图片,我们实现了一个简单的文件系统缓存。

// src/utils/cache.js
export async function getCache(content) {
  // 计算文件内容 Hash
  const hash = crypto.createHash('sha256').update(content).digest('hex');
  const cachePath = path.join(CACHE_DIR, hash);
  
  // 如果 Hash 命中,直接返回缓存文件,跳过压缩
  if (await fs.pathExists(cachePath)) {
    return await fs.readFile(cachePath);
  }
  return null;
}

🛠 快速上手指南

第一步:安装

npm install @zhaoshijun/compress -D

场景一:使用 CLI 批量压缩存量图片

如果你有一堆静态图片需要处理,直接运行 CLI 命令:

# 扫描当前目录,压缩图片并输出到 ./compressed 目录
npx @zhaoshijun/compress

# 常用参数
# --dry-run : 仅预览,不实际写入
# --backup  : 备份原文件

CLI 会在终端显示进度条和压缩前后的体积对比:

✔ assets/banner.jpg: 1.2 MB → 450 KB (Saved 62.5%)

场景二:集成到 Vite 项目

vite.config.js 中配置插件,即可在打包时自动瘦身:

import { defineConfig } from 'vite';
import { compressVitePlugin } from '@zhaoshijun/compress';

export default defineConfig({
  plugins: [
    compressVitePlugin({
      quality80// 压缩质量
      cachetrue  // 开启缓存
    })
  ]
});

以后每次运行 npm run build,你的产物图片就已经是最优体积了!


🔗 总结

工具化、自动化是提升前端工程效率的关键。@zhaoshijun/compress 通过简单的配置,解决了图片压缩这一高频痛点。

欢迎大家下载体验,如果有问题或建议,欢迎在评论区留言!

本文使用 markdown.com.cn 排版

3. 避坑+实战|Vue3 hoistStatic静态提升,让渲染速度翻倍的秘密

作者 boooooooom
2026年1月26日 15:52

在 Vue3 的编译优化体系中,静态提升(hoistStatic) 是核心性能优化手段之一。它通过在编译阶段识别并提取模板中的静态内容,避免每次组件渲染时重复创建、比对这些不变的节点,大幅减少运行时开销。本文将从“做了什么”“怎么做”“优化效果”三个维度,彻底讲清 hoistStatic 的底层逻辑与实际价值。

一、先搞懂:什么是“静态内容”?

在分析静态提升前,需明确 Vue3 对“静态内容”的定义:

  • 静态节点:内容完全固定、不会随响应式数据变化的节点(如 <div>Hello Vue3</div>);
  • 静态属性:值固定的属性(如 class="title"id="box");
  • 静态树:由多个静态节点组成的完整子树(如纯静态的导航栏、页脚)。

这些内容的特征是:组件生命周期内永远不会变化,若不做优化,每次组件重新渲染(如响应式数据更新)时,Vue 会重复创建这些节点的 VNode,并参与虚拟 DOM 比对,造成无意义的性能消耗。

二、hoistStatic 核心动作:3类静态内容的提升逻辑

Vue3 的编译器在开启 hoistStatic(默认开启)后,会对不同类型的静态内容执行针对性提升,核心目标是“将静态内容移出渲染函数,仅创建一次,复用多次”。

1. 基础动作:静态节点提升至渲染函数外部

核心逻辑:将单个静态节点的 VNode 创建逻辑,从组件的渲染函数(_render)中提取到外部,仅在组件初始化时创建一次,后续渲染直接复用该 VNode。

优化前(未开启静态提升)

每次渲染都会重新创建静态节点的 VNode:

// 编译后的渲染函数(简化版)
function render() {
  return createVNode('div', null, [
    // 静态节点:每次渲染都重新创建
    createVNode('p', { class: 'static' }, '静态文本'),
    // 动态节点:随数据变化
    createVNode('p', null, ctx.msg)
  ])
}

优化后(开启静态提升)

静态节点被提升到渲染函数外部,仅初始化一次:

// 静态节点被提升到外部,仅创建一次
const _hoisted_1 = createVNode('p', { class: 'static' }, '静态文本')

// 渲染函数中直接复用
function render() {
  return createVNode('div', null, [
    _hoisted_1, // 复用已创建的静态 VNode
    createVNode('p', null, ctx.msg)
  ])
}

2. 进阶动作:静态属性提升

对于静态属性(如固定的 classidstyle),Vue3 会将其提取为常量,避免每次创建 VNode 时重复创建属性对象。

优化示例

// 优化前:每次渲染创建新的属性对象
createVNode('div', { class: 'header', id: 'nav' }, '导航栏')

// 优化后:静态属性提升为常量
const _hoisted_2 = { class: 'header', id: 'nav' }
// 渲染时复用属性对象
createVNode('div', _hoisted_2, '导航栏')

3. 深度动作:静态树整体提升

若模板中存在连续的静态节点组成的“静态树”(如整个页脚、纯静态的侧边栏),hoistStatic 会将整个静态树作为一个整体提升,而非单个节点拆分,进一步减少内存占用和创建开销。

优化示例(静态树)

<!-- 模板中的静态树 -->
<footer>
  <div class="footer-logo">Vue3</div>
  <div class="footer-text">版权所有 © 2026</div>
</footer>

<!-- 编译后:整个静态树被提升为单个 VNode 常量 -->
const _hoisted_3 = createVNode('footer', null, [
  createVNode('div', { class: 'footer-logo' }, 'Vue3'),
  createVNode('div', { class: 'footer-text' }, '版权所有 © 2026')
])

// 渲染函数中直接复用整棵树
function render() {
  return createVNode('div', null, [
    // 其他动态内容
    _hoisted_3 // 复用静态树
  ])
}

三、静态提升的额外优化:跳过虚拟 DOM 比对

Vue3 的虚拟 DOM 比对(patch)过程中,若识别到节点是“静态提升节点”,会直接跳过比对逻辑——因为已知这些节点不会变化,无需消耗性能检查属性、子节点是否更新。

核心逻辑伪代码:

function patch(n1, n2) {
  // 静态节点:直接跳过比对,复用即可
  if (n2.shapeFlag & ShapeFlags.STATIC) {
    return
  }
  // 动态节点:执行常规比对逻辑
  // ...
}

四、hoistStatic 的生效规则与避坑点

1. 生效条件

  • 仅对编译时能确定的静态内容生效(如固定文本、固定属性),含动态插值({{ msg }})、动态指令(v-if/v-for)的节点不参与提升;
  • Vue3 默认为生产环境开启 hoistStatic,开发环境可通过 compilerOptions 手动配置;
  • 单个静态节点需满足“非根节点且无动态绑定”,才会被提升(根节点提升无意义)。

2. 常见坑点

  • 误区1:认为“所有静态内容都会被提升”——Vue3 对极短的静态节点(如单个 <span>123</span>)可能不提升,因为提升的内存开销大于收益;
  • 误区2:静态内容中混入动态指令(如 v-on:click)——含动态指令的节点会被判定为动态节点,无法提升;
  • 误区3:手动关闭 hoistStatic——除非有特殊编译需求,否则不要关闭,会显著降低渲染性能。

五、实战验证:静态提升的性能收益

以一个包含 100 个静态节点 + 1 个动态节点的组件为例:

  • 未开启静态提升:每次渲染需创建 101 个 VNode,执行 101 次虚拟 DOM 比对;
  • 开启静态提升:每次渲染仅创建 1 个动态 VNode,100 个静态 VNode 复用,且跳过 100 次比对。

实测数据(Vue3 官方基准测试):

  • 渲染耗时降低约 30%~50%;
  • 内存占用减少约 20%(避免重复创建 VNode 和属性对象)。

总结

关键点回顾

  1. hoistStatic 核心是编译阶段提取静态内容,将其移出渲染函数,仅初始化一次、渲染时复用;
  2. 优化维度包括:静态节点、静态属性、静态树的提升,以及跳过静态节点的虚拟 DOM 比对;
  3. 仅对编译时确定的静态内容生效,含动态逻辑的节点无法提升,且需避免过度依赖静态提升优化动态场景。

Vue3 的静态提升看似是“细节优化”,实则是从编译层面减少运行时无意义的计算,这也是 Vue3 相比 Vue2 渲染性能大幅提升的核心原因之一。理解其底层逻辑,能帮助你在开发中更合理地编写模板(如拆分静态/动态内容),最大化利用该优化特性。

用 Three.js + D3.js 实现可交互 3D 中国地图(省份聚焦 / 飞线 / 光柱)

作者 scorpioop
2026年1月26日 15:51

用 Three.js + D3.js 实现可交互 3D 中国地图(省份聚焦 / 飞线 / 光柱)

本文将完整拆解一个 Three.js + D3.js 实现的 3D 中国地图项目,包含:

  • GeoJSON 转 3D 地图
  • 省份点击高亮 + 相机平滑聚焦
  • 光柱数据可视化
  • 城市飞线动画
  • CSS2D 标签系统

一、整体效果预览

ScreenShot_2026-01-26_150821_367.png


二、技术栈说明

Three.js        // 3D 渲染
D3-geo          // 经纬度 → 平面坐标
CSS2DRenderer   // DOM 标签
GeoJSON         // 中国地图数据
React Hooks     // 生命周期管理

三、GeoJSON → Three.js 3D 地图

通过geoJson数据创建地图形状,geoJson数据可以从这里获取

// 声明一个函数用于创建地图形状
  function createSichuanShape(scene: THREE.Scene) {
    // 解析json数据
    const jsonData = JSON.parse(chinaJson);
    
    // 创建一个分组,把地图中的多个图形放在一个组中,便于管理
    const map = new THREE.Group();
    // 使用d3把经纬度转换成平面坐标,中心点为成都
    const projection = d3.geoMercator().center([104.065735, 30.659462]);
    // 遍历json数据中的features
    jsonData.features.forEach((feature: any, i: any) => {
      // 获取feature中的geometry数据,方便引用

      const geometry = feature.geometry;
      const type = geometry.type;
      // 创建分组,把同一个市的形状放在一个分组,便于管理
      const city = new THREE.Group();
      if (type === "MultiPolygon" || type === "Polygon") {
        
        // 遍历geometry中的数据
        geometry.coordinates.forEach((multipolygon: any) => {
          // 创建形状,用于展示地理形状
          const shape = new THREE.Shape();
          // 存储每个坐标,用于绘制边界线
          const arr: any = [];
          if (type === "Polygon") {
            multipolygon.forEach((item: any, index: any) => {
              // 使用d3转换坐标
              const [x, y] = projection.translate([0, 0])(item) as any;
              // 根据转换后的坐标绘制形状
              if (index === 0) {
                shape.moveTo(x, y);
              } else {
                shape.lineTo(x, y);
              }
              arr.push(x, y, 3.3);
            });
            // 画边界线
            createLine(arr, city);
          } else {
            multipolygon.forEach((polygon: any) => {
              polygon.forEach((item: any, index: any) => {
                // 使用d3转换坐标
                const [x, y] = projection.translate([0, 0])(item) as any;
                // 根据转换后的坐标绘制形状
                if (index === 0) {
                  shape.moveTo(x, y);
                } else {
                  shape.lineTo(x, y);
                }
                arr.push(x, y, 3.3);
              });
            });
            // 绘制边界线
            createLine(arr, city);
          }

          // 创建 ExtrudeGeometry用于显示3d效果
          const extrudGeometry = new THREE.ExtrudeGeometry(shape, {
            depth: 3, // 地图的厚度
          });
          
          const material = new THREE.ShaderMaterial({
            uniforms: {
              uTopColor: { value: new THREE.Color("#8bcee6") },
              uBottomColor: { value: new THREE.Color("#12357d") },
              uSelected: { value: 0.0 },
              minY: { value: -90 },
              maxY: { value: 10 },
            },
            vertexShader: `
   varying float vY;
    void main() {
      vY = position.y;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
            fragmentShader: `
    uniform vec3 uTopColor;
    uniform vec3 uBottomColor;
    uniform float uSelected;
    uniform float minY;
    uniform float maxY;
    varying float vY;

    void main() {
      float t = clamp((vY - minY) / (maxY - minY), 0.0, 1.0);
      vec3 color = mix(uBottomColor, uTopColor, t);

      // 被选中时提亮
      if (uSelected > 0.5) {
        color = mix(color, vec3(1.0, 0.9, 0.8), 0.3);
      }

      gl_FragColor = vec4(color, 1.0);
    }
  `,
          });

          // 创建物体,用于显示地图
          const mesh = new THREE.Mesh(extrudGeometry, material);
          mesh.position.set(0, 0, 0);
          mesh.userData = {
            name: feature.properties.name,
            feature,
          };
          // 添加到分组
          city.add(mesh);
        });
      }

      if (feature.properties.name) {
        createProvinceLabel(
          feature.properties.name,
          feature.properties.centroid
            ? feature.properties.centroid
            : feature.properties.center,
          projection,
          city
        );
      }

      // 添加到分组
      map.add(city);
    });
    // 添加到场景
    scene.add(map);
    map.scale.y = -1;
    barsGroup.current = createLightBars(lightBarData, projection, scene);
    return map;
  }

四、省份点击高亮和视角移动

省份点击时会有颜色变化,以及相机视角会变换到省份中心

const onClick = (event: MouseEvent) => {
        // 获取渲染区域的边界,用于正确获取鼠标点击位置
        const rect = renderer.domElement.getBoundingClientRect();

        // 这段代码的作用是将鼠标点击在canvas画布上的像素坐标转换为标准化设备坐标(Normalized Device Coordinates, NDC),范围是[-1, 1]
        mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
        mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;

        raycaster.setFromCamera(mouse, camera);
        // 只点击到mesh有反应,点击到线和label没用

        const meshes: THREE.Mesh[] = [];
        map.traverse((obj) => {
          if ((obj as THREE.Mesh).isMesh) {
            meshes.push(obj as THREE.Mesh);
          }
        });

        const intersects = raycaster.intersectObjects(meshes, true);

        if (intersects.length > 0) {
          const mesh = intersects[0].object as THREE.Mesh;
          onSelectProvince(mesh);
          focusProvince(mesh, camera, controls);
        }
      };
      // 省份变色
  function onSelectProvince(mesh: THREE.Mesh) {
   
    if (activeMesh.current) {
      (
        activeMesh.current.material as THREE.ShaderMaterial
      ).uniforms.uSelected.value = 0;
    }
    
    // 这样设置后,根据material中的设置,省份颜色就会发生改变
    (mesh.material as THREE.ShaderMaterial).uniforms.uSelected.value = 1;

    activeMesh.current = mesh;

    console.log("选中省份:", mesh.userData.name);
  }
  // 聚焦省份
  function focusProvince(
    mesh: THREE.Object3D,
    camera: THREE.PerspectiveCamera,
    controls: OrbitControls
  ) {
    const center = getMeshCenter(mesh);
    const camPos = calcCameraPosition(center, 50);

    moveCameraTo(camera, controls, camPos, center);
  }
  // 获取mesh的中心点
  function getMeshCenter(mesh: THREE.Object3D) {
    const box = new THREE.Box3().setFromObject(mesh);
    const center = new THREE.Vector3();
    box.getCenter(center);
    return center;
  }
  function calcCameraPosition(target: THREE.Vector3, offset = 40) {
    return new THREE.Vector3(target.x, target.y - offset, target.z + offset);
  }

  function moveCameraTo(
    camera: THREE.Camera,
    controls: OrbitControls,
    newCamPos: THREE.Vector3,
    newTarget: THREE.Vector3
  ) {
    camFrom.copy(camera.position);
    camTo.copy(newCamPos);

    targetFrom.copy(controls.target);
    targetTo.copy(newTarget);

    t = 0;
    moving = true;
  }
   const animate = () => {
        ...前面已省略
        if (moving) {
          t += 0.03;
          if (t >= 1) {
            t = 1;
            moving = false;
          }
          // 让相机位置在 `camFrom` 和 `camFrom` 之间,按 `t` 的比例“线性插值”移动
          camera.position.lerpVectors(camFrom, camTo, t);
          controls.target.lerpVectors(targetFrom, targetTo, t);
          controls.update();
        }
        
        rafIdRef.current = requestAnimationFrame(animate);
      };

只对 Mesh 做拾取,避免误点到线或 label。


五、光柱数据可视化

光柱本体

const geometry = new THREE.CylinderGeometry(0.6, 0.6, height);

顶部发光 Sprite

const glow = new THREE.Sprite(
  new THREE.SpriteMaterial({ map: glowTexture })
);

数值标签

使用 CSS2DObject,比 Canvas 字体清晰得多。


六、飞线动画实现

 // 创建飞线
  function createFlyLine(position1: any, position2: any, scene: THREE.Scene) {
    function createFlyCurve(
      start: THREE.Vector3,
      end: THREE.Vector3,
      height = 20
    ) {
      const mid = start.clone().lerp(end, 0.5);
      mid.z += height; // 抬高形成弧线
      // 创建弧线
      return new THREE.QuadraticBezierCurve3(start, mid, end);
    }

    const start = new THREE.Vector3(...position1);
    const end = new THREE.Vector3(...position2);

    const curve = createFlyCurve(start, end, 30);

    const geometry = new THREE.TubeGeometry(curve, 100, 0.15, 8, false);

    const texture = new THREE.TextureLoader().load(flyLineImg);
    texture.wrapS = THREE.RepeatWrapping;
    texture.repeat.set(1, 1);

    const material = new THREE.MeshBasicMaterial({
      map: texture,
      transparent: true,
      depthWrite: false,
      blending: THREE.AdditiveBlending,
    });

    const flyLine = new THREE.Mesh(geometry, material);
    flyLineTexture.current.push(texture);
    scene.add(flyLine);
  }

七、地图省份名称标签

// 创建省份标签
  function createProvinceLabel(
    name: string,
    center: [number, number],
    projection: any,
    map: THREE.Group
  ) {
    const parent = document.createElement("div");
    const div = document.createElement("div");
    div.className = "province-label";
    div.textContent = name;
    parent.appendChild(div);

    const label = new CSS2DObject(parent);

    const [x, y] = projection(center);
    label.position.set(x, y, 3); // z 稍微抬起,Y轴翻转以匹配地图

    map.add(label);
  }

特点:

  • 不参与 WebGL
  • 不会被遮挡
  • 适合文字、数值、UI

十、资源与生命周期清理(非常重要)

cancelAnimationFrame(rafId);
geometry.dispose();
material.dispose();
renderer.forceContextLoss();

Three.js 不会帮你自动回收显存,React 项目必须手动清理。


从“一句话描述”到“专业级画作”:文生图、图生图、局部重绘、智能扩图一站式搞定

2026年1月26日 15:51

AUTOMATIC1111/stable-diffusion-webui 深度技术解读

1. 整体介绍

1.1 项目概况

AUTOMATIC1111/stable-diffusion-webui 是基于 Gradio 框架构建的 Stable Diffusion 模型 Web 图形界面,是目前 GitHub 上最受欢迎的 Stable Diffusion 前端实现之一(截至当前,GitHub stars 超过 100k,forks 超过 20k)。项目通过将复杂的命令行操作封装为直观的 Web 界面,大幅降低了使用先进生成式 AI 模型的技术门槛。

1.2 核心功能定位

  • 核心价值:提供本地化、一体化、可扩展的 Stable Diffusion 操作环境
  • 技术定位:介于原始 Stable Diffusion 代码库与云端服务之间的中间层
  • 用户界面:基于 Gradio 构建的响应式 Web 界面,支持实时交互

1.3 解决的核心问题

传统方案痛点

  1. 配置复杂:原始 Stable Diffusion 需要手动安装 PyTorch、配置 CUDA、管理依赖版本
  2. 操作门槛高:依赖命令行参数和 Python 脚本,非技术用户难以使用
  3. 功能分散:图像生成、模型管理、后处理等工具分散在不同项目中
  4. 扩展困难:社区贡献难以集成,用户需要手动合并代码

本项目解决方案

  1. 一体化安装:通过 launch.py 自动处理环境依赖和模型下载
  2. 可视化操作:将命令行参数转化为 Web 表单控件
  3. 模块化架构:通过插件系统支持功能扩展
  4. 标准化接口:提供统一的 API 和配置管理

1.4 商业价值分析

开发成本估算

  • 核心框架开发:约 6-8 人月
  • Gradio 深度集成:约 2-3 人月
  • 扩展系统设计:约 3-4 人月
  • 测试与优化:约 2-3 人月
  • 总计估算:约 13-18 人月的高级开发投入

效益分析逻辑

  1. 用户时间节省:相比手动配置,每个用户平均节省 4-8 小时初始化时间
  2. 技术门槛降低:使非专业开发者能够使用先进 AI 模型
  3. 社区生态价值:通过扩展系统形成正反馈循环,吸引开发者贡献
  4. 模型普及推动:加速 Stable Diffusion 生态发展,间接促进硬件和云服务需求

2. 详细功能拆解

2.1 技术架构分层

┌─────────────────────────────────────┐
│            Web 界面层               │
│  (Gradio Blocks + 自定义组件)       │
├─────────────────────────────────────┤
│          应用逻辑层                  │
│  (脚本回调 + 状态管理 + 队列控制)    │
├─────────────────────────────────────┤
│          核心服务层                  │
│  (模型加载 + 图像处理 + 扩展管理)    │
├─────────────────────────────────────┤
│          基础设施层                  │
│  (环境管理 + 依赖安装 + 配置持久化)  │
└─────────────────────────────────────┘

2.2 核心功能模块

1. 启动与环境管理 (launch_utils.py)

  • 自动检测 Python 版本和 CUDA 环境
  • 智能安装 PyTorch 和依赖包
  • Git 子模块管理和版本控制
  • 扩展插件自动安装

2. Web 服务器与路由 (webui.py)

  • Gradio 应用生命周期管理
  • API 服务器模式支持
  • 中间件配置(CORS、GZip)
  • 会话状态持久化

3. 全局状态管理 (shared.py)

  • 单例模式管理模型实例
  • 配置选项的集中存储
  • 线程安全的进度状态跟踪
  • 主题和界面偏好管理

4. 模块初始化系统 (initialize.py)

  • 延迟加载优化启动时间
  • 动态模块导入和错误处理
  • 配置恢复和状态回滚
  • 钩子系统用于扩展点

3. 技术难点与解决方案

3.1 环境依赖复杂性管理

难点:Stable Diffusion 依赖特定版本的 PyTorch、xformers、CUDA 工具链,版本冲突常见。

解决方案

# launch_utils.py 中的版本适配逻辑
def prepare_environment():
    # 根据平台和硬件自动选择 torch 安装命令
    if args.use_ipex:
        if platform.system() == "Windows":
            # Windows + Intel Arc GPU 的特殊处理
            torch_command = "pip install 定制化IPEX包..."
        else:
            # Linux 的官方 IPEX 包
            torch_command = "pip install torch==2.0.0a0 intel-extension-for-pytorch..."
    else:
        # 标准 NVIDIA CUDA 安装
        torch_index_url = "https://download.pytorch.org/whl/cu121"
        torch_command = f"pip install torch==2.1.2 torchvision==0.16.2 --extra-index-url {torch_index_url}"
    
    # 执行安装并验证
    run(torch_command, "Installing torch and torchvision", "Couldn't install torch", live=True)

3.2 模型热加载与内存管理

难点:大模型(通常 2-7GB)加载耗时,多个模型切换时内存容易溢出。

解决方案

# shared.py 中的模型状态管理
class SharedState:
    def __init__(self):
        self.sd_model = None  # 当前加载的模型
        self.models_cache = {}  # 模型缓存(可选)
        self.current_model_hash = None
        
    def load_model(self, checkpoint_path):
        # 卸载当前模型释放显存
        if self.sd_model is not None:
            self.unload_model()
        
        # 加载新模型
        model = load_model_from_checkpoint(checkpoint_path)
        
        # 应用优化(xformers、注意力优化等)
        if args.xformers:
            apply_xformers_optimizations(model)
        
        self.sd_model = model
        self.current_model_hash = calculate_hash(checkpoint_path)

3.3 扩展系统设计与安全性

难点:支持第三方扩展的同时保证系统稳定性和安全性。

解决方案

# launch_utils.py 中的扩展安装器
def run_extension_installer(extension_dir):
    path_installer = os.path.join(extension_dir, "install.py")
    if not os.path.isfile(path_installer):
        return
    
    try:
        # 隔离环境运行安装脚本
        env = os.environ.copy()
        env['PYTHONPATH'] = f"{script_path}{os.pathsep}{env.get('PYTHONPATH', '')}"
        
        # 执行安装并捕获输出
        stdout = run(f'"{python}" "{path_installer}"',
                    errdesc=f"Error running install.py for extension {extension_dir}",
                    custom_env=env).strip()
        if stdout:
            print(stdout)  # 日志记录安装过程
    except Exception as e:
        # 优雅的错误处理,不破坏主程序
        errors.report(str(e))

3.4 实时进度反馈与队列管理

难点:长时间图像生成任务需要实时进度更新,同时支持并发请求。

解决方案

# webui.py 中的队列和进度管理
def webui():
    from modules.call_queue import queue_lock
    
    # 创建 Gradio 界面
    shared.demo = ui.create_ui()
    
    # 配置任务队列
    if not cmd_opts.no_gradio_queue:
        shared.demo.queue(64)  # 允许最多64个并发请求
    
    # 进度API设置
    progress.setup_progress_api(app)
    
    # 实时状态更新循环
    while True:
        server_command = shared.state.wait_for_server_command(timeout=5)
        if server_command == "stop":
            break
        elif server_command == "restart":
            # 优雅重启逻辑
            handle_restart()

4. 详细设计图

4.1 系统架构图

graph TB
    A[用户浏览器] --> B[Gradio HTTP Server]
    B --> C{路由分发}
    
    C -->|API请求| D[FastAPI Endpoints]
    C -->|UI请求| E[Gradio Blocks UI]
    
    D --> F[API Handler]
    E --> G[UI Event Handler]
    
    F --> H[Task Queue]
    G --> H
    
    H --> I[Model Executor]
    I --> J[Stable Diffusion Model]
    I --> K[Extension System]
    
    J --> L[Image Processor]
    K --> L
    
    L --> M[Result Cache]
    M --> N[Response Formatter]
    
    N -->|JSON| O[API Client]
    N -->|Image/HTML| P[Web UI]
    
    subgraph "核心服务"
        H
        I
        J
        L
    end
    
    subgraph "扩展系统"
        K
        Q[Custom Scripts]
        R[Extra Networks]
        S[UI Extensions]
    end
    
    subgraph "基础设施"
        T[Config Manager]
        U[Model Loader]
        V[Environment Manager]
    end

4.2 启动序列图

sequenceDiagram
    participant U as User
    participant L as launch.py
    participant LU as launch_utils
    participant W as webui.py
    participant I as initialize.py
    participant S as shared.py
    
    U->>L: 执行 python launch.py
    L->>LU: main()
    LU->>LU: prepare_environment()
    
    alt 环境检查
        LU->>LU: check_python_version()
        LU->>LU: 安装依赖包
        LU->>LU: 克隆模型仓库
    end
    
    LU->>W: start()
    
    alt API模式
        W->>W: api_only()
        W->>I: initialize()
        I->>S: 初始化全局状态
        W->>W: 创建FastAPI应用
        W->>W: 启动API服务器
    else WebUI模式
        W->>W: webui()
        W->>I: initialize()
        I->>S: 初始化全局状态
        W->>W: create_ui()
        W->>W: demo.launch()
        W->>W: 进入主事件循环
    end
    
    W-->>U: 服务就绪

4.3 核心类图

classDiagram
    class LaunchUtils {
        -python: str
        -git: str
        -index_url: str
        +prepare_environment()
        +run_pip()
        +git_clone()
        +is_installed()
        +run()
    }
    
    class WebUI {
        -startup_timer
        +api_only()
        +webui()
        -create_api()
    }
    
    class SharedState {
        -sd_model
        -opts
        -state
        +load_model()
        +unload_model()
        +get_progress()
    }
    
    class Options {
        -data: dict
        +onchange()
        +save()
        +load()
    }
    
    class ScriptCallbacks {
        +before_ui_callback()
        +app_started_callback()
        +script_unloaded_callback()
    }
    
    class ExtensionManager {
        -extensions_dir
        +list_extensions()
        +run_installers()
        +load_extension()
    }
    
    LaunchUtils --> WebUI : 启动
    WebUI --> SharedState : 使用
    SharedState --> Options : 包含
    WebUI --> ScriptCallbacks : 回调
    ScriptCallbacks --> ExtensionManager : 管理

5. 核心函数解析

5.1 环境准备函数 (prepare_environment)

def prepare_environment():
    """核心环境初始化函数,处理所有前置依赖"""
    # 1. 配置 Torch 安装源和版本
    torch_index_url = os.environ.get('TORCH_INDEX_URL', "https://download.pytorch.org/whl/cu121")
    torch_command = os.environ.get('TORCH_COMMAND', 
        f"pip install torch==2.1.2 torchvision==0.16.2 --extra-index-url {torch_index_url}")
    
    # 2. 硬件特定优化(Intel IPEX)
    if args.use_ipex:
        if platform.system() == "Windows":
            # Windows + Intel Arc 的特殊构建
            url_prefix = "https://github.com/Nuullll/intel-extension-for-pytorch/releases/download/..."
            torch_command = f"pip install {url_prefix}/torch-2.0.0a0...whl"
    
    # 3. 基础依赖检查与安装
    if not args.skip_torch_cuda_test and not check_run_python("import torch; assert torch.cuda.is_available()"):
        raise RuntimeError('Torch is not able to use GPU')
    
    # 4. 克隆必要的模型仓库
    git_clone(assets_repo, repo_dir('stable-diffusion-webui-assets'), "assets", assets_commit_hash)
    git_clone(stable_diffusion_repo, repo_dir('stable-diffusion-stability-ai'), 
              "Stable Diffusion", stable_diffusion_commit_hash)
    
    # 5. 安装 Python 依赖包
    requirements_file = os.environ.get('REQS_FILE', "requirements_versions.txt")
    if not requirements_met(requirements_file):
        run_pip(f"install -r \"{requirements_file}\"", "requirements")
    
    # 6. 扩展插件安装
    if not args.skip_install:
        run_extensions_installers(settings_file=args.ui_settings_file)

5.2 模块初始化函数 (initialize)

def initialize():
    """核心模块初始化,实现按需加载"""
    from modules import initialize_util
    
    # 1. 系统级修复和配置
    initialize_util.fix_torch_version()        # 修复 torch 版本字符串
    initialize_util.fix_asyncio_event_loop_policy()  # 修复异步事件循环
    initialize_util.configure_sigint_handler() # 配置信号处理
    
    # 2. 模型系统初始化
    from modules import sd_models
    sd_models.setup_model()  # 设置模型加载路径和缓存
    
    # 3. 后处理模型加载(按需)
    from modules import codeformer_model, gfpgan_model
    codeformer_model.setup_model(cmd_opts.codeformer_models_path)
    gfpgan_model.setup_model(cmd_opts.gfpgan_models_path)
    
    # 4. 扩展系统初始化
    initialize_rest(reload_script_modules=False)

def initialize_rest(*, reload_script_modules=False):
    """辅助初始化函数,支持重载"""
    from modules import scripts, extensions, sd_models
    
    # 1. 加载采样器配置
    from modules import sd_samplers
    sd_samplers.set_samplers()
    
    # 2. 扩展脚本动态加载
    with startup_timer.subcategory("load scripts"):
        scripts.load_scripts()  # 从 extensions_dir 加载用户脚本
    
    # 3. 模型列表刷新
    if not shared.cmd_opts.ui_debug_mode:
        sd_models.list_models()  # 扫描 models 目录
    
    # 4. 后台线程加载主模型(优化启动体验)
    if not shared.cmd_opts.skip_load_model_at_start:
        Thread(target=load_model).start()  # 异步加载避免界面卡顿

5.3 Gradio 应用启动函数 (webui)

def webui():
    """主 Web UI 启动函数,管理完整的应用生命周期"""
    from modules.shared_cmd_options import cmd_opts
    launch_api = cmd_opts.api
    
    # 1. 系统初始化
    initialize.initialize()
    
    # 2. 创建 Gradio 界面组件
    from modules import shared, ui, script_callbacks
    shared.demo = ui.create_ui()  # 构建所有UI标签页和控件
    
    # 3. 配置任务队列(支持并发)
    if not cmd_opts.no_gradio_queue:
        shared.demo.queue(64)  # 设置队列大小
    
    # 4. 启动 Gradio 服务器
    app, local_url, share_url = shared.demo.launch(
        share=cmd_opts.share,                    # 是否生成公网链接
        server_name=initialize_util.gradio_server_name(),  # 绑定地址
        server_port=cmd_opts.port,               # 端口号
        auth=gradio_auth_creds,                  # 身份验证
        inbrowser=auto_launch_browser,           # 自动打开浏览器
        prevent_thread_lock=True,                # 不阻塞主线程
        root_path=f"/{cmd_opts.subpath}" if cmd_opts.subpath else ""
    )
    
    # 5. 安全加固:移除过于宽松的 CORS 设置
    app.user_middleware = [x for x in app.user_middleware 
                          if x.cls.__name__ != 'CORSMiddleware']
    initialize_util.setup_middleware(app)  # 应用自定义中间件
    
    # 6. 注册 API 端点
    if launch_api:
        create_api(app)  # 创建 RESTful API
    
    # 7. 扩展回调系统
    script_callbacks.app_started_callback(shared.demo, app)
    
    # 8. 主事件循环(支持重启)
    try:
        while True:
            server_command = shared.state.wait_for_server_command(timeout=5)
            if server_command == "stop":
                break
            elif server_command == "restart":
                handle_restart()  # 优雅重启逻辑
    except KeyboardInterrupt:
        print('Caught KeyboardInterrupt, stopping...')
    
    # 9. 清理资源
    shared.demo.close()

6. 同类技术对比

6.1 与 ComfyUI 对比

特性 AUTOMATIC1111 WebUI ComfyUI
学习曲线 较低,传统表单界面 较高,节点式工作流
扩展性 插件系统,Python脚本 节点系统,可视化编程
性能 优化良好,支持低显存 需要更多显存,但流程更灵活
社区生态 极活跃,扩展丰富 增长迅速,工作流分享多
适用场景 常规图像生成、快速迭代 复杂流程、批量处理、研究

6.2 与 DiffusionBee (macOS) 对比

维度 WebUI DiffusionBee
安装复杂度 中等,需要Python环境 简单,直接安装
功能完整性 完整,支持所有高级功能 基础,核心生成功能
可定制性 极高,完全开源可修改 有限,闭源软件
跨平台 Windows/Linux/macOS macOS 专属
更新频率 每日更新,快速迭代 较慢,稳定发布

7. 技术演进建议

7.1 架构优化方向

  1. 模块解耦:进一步分离界面逻辑与生成逻辑
  2. 微服务化:考虑将模型服务、UI服务、扩展服务分离部署
  3. 配置即代码:支持声明式配置,便于版本控制和团队协作

7.2 性能提升建议

  1. 模型预热:后台预加载常用模型减少等待时间
  2. 结果缓存:实现生成结果的智能缓存和复用
  3. 渐进式加载:超大界面按需加载组件,提升初次打开速度

7.3 安全增强

  1. 扩展沙箱:对第三方脚本运行环境隔离
  2. 输入验证:加强提示词和参数的安全检查
  3. 访问控制:更细粒度的权限管理系统

总结

AUTOMATIC1111/stable-diffusion-webui 通过精心设计的模块化架构和稳健的工程实现,成功地将复杂的 Stable Diffusion 模型封装为易用的 Web 应用。其核心价值不仅在于功能丰富性,更在于:

  1. 工程完备性:从环境管理到错误处理都体现了生产级软件的考量
  2. 扩展友好性:设计良好的回调系统和配置管理支持生态发展
  3. 渐进式复杂度:界面设计既满足初学者也能服务高级用户
  4. 社区驱动:开源协作模式确保了快速迭代和问题修复

项目在技术实现上平衡了易用性与灵活性,通过合理的抽象层设计,使得底层模型升级和界面功能扩展能够相对独立地进行,这是其能够长期保持活跃和领先的关键架构优势。

@types 包的工作原理与最佳实践

作者 wuhen_n
2026年1月26日 15:45

当我们在使用 TypeScript 开发时,经常需要安装 @types/xxx 来获得第三方库的类型支持。这些神秘的 @types 包是如何工作的?DefinitelyTyped 又是什么?本篇文章将揭开这些类型包的神秘面纱。

DefinitelyTyped:TypeScript的"类型宝库"

什么是DefinitelyTyped?

DefinitelyTyped(简称DT),是 GitHub 上的一个开源项目,专门为 JavaScript 库提供高质量的 TypeScript 类型定义。它是 TypeScript 生态系统中最重要的基础设施之一。

举个例子,比如我们正在使用一个纯 JavaScript 库,例如 lodash.js ,这里面是没有类型信息的。在 JavaScript 中是可以工作的:_.map([1,2,3], x => x * 2); 。 但在 TypeScript 中:import _ from 'lodash';,就会报错:找不到模块'lodash'的声明文件 。这时我们就需要安装类型定义:npm install --save-dev @types/lodash ,然后 TypeScript 就能理解 lodash 了。

类型包是如何创建的?

假设我们有一个原始的 JavaScript 库:tiny-validator.js

function validateEmail(email) {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

function validatePhone(phone) {
  return /^\d{10,11}$/.test(phone);
}

function validateURL(url) {
  try {
    new URL(url);
    return true;
  } catch {
    return false;
  }
}

module.exports = {
  validateEmail,
  validatePhone,
  validateURL,
  version: '1.0.0'
};

现在我们要为它创建类型:

// 导出类型
export interface ValidationResult {
  isValid: boolean;
  message?: string;
}

// 导出函数
export function validateEmail(email: string): boolean;
export function validateEmail(email: string, strict: true): ValidationResult;

export function validatePhone(phone: string): boolean;
export function validatePhone(phone: string, countryCode: string): boolean;

export function validateURL(url: string): boolean;
export function validateURL(url: string, options: { requireProtocol: boolean }): boolean;

// 导出常量
export const version: string;

// 默认导出
declare const validator: {
  validateEmail: typeof validateEmail;
  validatePhone: typeof validatePhone;
  validateURL: typeof validateURL;
  version: typeof version;
};

export default validator;

@types包的工作原理

类型包的发布流程

假设我们要发布一个 @types/react 包:

  1. 在DefinitelyTyped提交PR,提交更改到 types/react/
  2. CI运行测试:
    • 编译类型检查
    • 运行类型测试
    • 验证格式
  3. 维护者审查
  4. 合并PR
  5. 自动发布到 npm 为 @types/react

类型包如何被TypeScript识别

TypeScript 会按以下顺序查找类型(以 lodash 为例):

  1. 当前文件的 .d.ts 声明
  2. 项目中的 .d.ts 文件
  3. node_modules/@types/lodash/index.d.ts:DefinitelyTyped提供的
  4. node_modules/lodash/package.json 中的 "types" 字段
  5. node_modules/lodash/index.d.ts:库自带的类型

当然,我们也可以通过配置 tsconfig.json,影响相关的顺序:

{
  "compilerOptions": {
    "typeRoots": [
      "./node_modules/@types",  // 默认包含
      "./custom-types"          // 自定义类型目录
    ],
    "types": [                  // 指定要包含的类型包
      "node",
      "lodash",
      "react"
    ]
  }
}

版本管理:@types包的命名规则

@types包的版本与对应库的版本关联,命名规则为:@types/<库名> ,可以看下面的示例:

原始库 类型包 说明
lodash @types/lodash 与lodash主版本对应
react @types/react 与react版本对应
node @types/node 与Node.js版本对应

当然,也存在特殊情况:如带作用域的包:@angular/core → @types/angular__core

类型包的版本管理

@types/node的版本管理

@types/node 是所有 @types 包中最特殊的一个,因为它与Node.js运行时版本紧密绑定。

// package.json 中的依赖
{
  "devDependencies": {
    // @types/node的版本应该与你的Node.js版本匹配
    "@types/node": "^18.0.0",  // 对应Node.js 18.x
    
    // 其他类型包
    "@types/express": "^4.17.0",
    "@types/lodash": "^4.14.0"
  }
}
// @types/node的版本演进示例
// Node.js 16.x 的类型
declare module "fs/promises" {
  function readFile(path: string): Promise<Buffer>;
  // Node 16的API
}

// Node.js 18.x 新增了fetch API
declare global {
  // Node 18新增了全局fetch
  function fetch(input: RequestInfo, init?: RequestInit): Promise<Response>;
  
  interface Request {}
  interface Response {}
}

这就是为什么我们在开发时需要:

  • Node.js 16 → @types/node@16.x
  • Node.js 18 → @types/node@18.x
  • Node.js 20 → @types/node@20.x

类型包的版本兼容性

类型包版本不匹配/不兼容的问题,应该是我们在使用类型包时遇到的最常见也是最麻烦的问题。为什么会出现这样的问题呢?例如我们的项目中使用 lodash@4.17.21 ,但实际上 @types/lodash 已经更新到了新版本,这样就出现了版本兼容性问题。

解决方案

指定精确版本
npm install @types/lodash@4.14.0  # 安装特定版本
查看库的package.json
{
  "name": "my-library",
  "version": "2.0.0",
  "devDependencies": {
    "@types/lodash": "^4.14.0"  # 推荐的类型版本
  }
}
使用peerDependencies
{
  "name": "@types/my-library",
  "peerDependencies": {
    "my-library": ">=2.0.0 <3.0.0"  # 要求原库版本
  }
}

如何选择合适的@types版本

1. 检查你安装的库版本

// package.json
{
  "dependencies": {
    "react": "17.0.2",      // React 17.0.2
    "lodash": "4.17.21"     // Lodash 4.17.21
  }
}

2. 查找对应的@types版本

例如: 对于React 18,应该使用 @types/react@18.x.x 对于Lodash 4,应该使用 @types/lodash@4.x.x

3. 查看版本历史

可以通过npm查看版本信息:

npm view @types/react versions  # 查看所有版本
npm view @types/react@18.0.0    # 查看特定版本信息

4. 在package.json中指定

{
  "devDependencies": {
    "@types/react": "17.0.0",    // 匹配React 17.0.x
    "@types/lodash": "4.14.0"    // Lodash 4的类型
  }
}

5. 处理冲突

如果库A依赖 @types/lodash@4.14.0,而库B依赖 @types/lodash@4.17.0 ,TypeScript会自动选择较高的版本(4.17.0)。这在大多数情况下是安全的,因为类型定义是向后兼容的。

常见问题

多个类型包冲突

场景:两个库都提供了相同的类型定义,例如: 在 @types/jquery@types/some-other-plugin 两个包中都声明了全局的 $,导致冲突!

解决方案

1. 使用模块声明而不是全局声明
declare module "jquery-plugin" {
  import $ from "jquery";
  
  // 使用导入的$,而不是声明全局的$
  interface JQuery {
    myPlugin(): JQuery;
  }
}
2. 合并声明

如果两个声明不冲突,可以共存:

interface JQuery {
  // 来自jquery
  hide(): JQuery;
  show(): JQuery;
  
  // 来自plugin1
  plugin1Method(): JQuery;
  
  // 来自plugin2  
  plugin2Method(): JQuery;
}
3. 在tsconfig中排除有问题的类型
{
  "compilerOptions": {
    "types": [
      "jquery",           // 包含jquery
      // "jquery-plugin"   // 排除冲突的插件类型
    ]
  }
}

类型包版本不匹配

场景:类型包版本与库版本不匹配,例如库版本为 lodash@4.17.21 ,而类型包版本为 @types/lodash@4.14.0 。由于类型包版本较旧,导致缺失某个API,直接使用新版API会报错。

解决方案

1. 升级类型包
npm update @types/lodash@latest
2. 临时补充类型定义
// custom-types/lodash-extensions.d.ts
import 'lodash';

declare module 'lodash' {
  interface LoDashStatic {
    // 补充缺失的方法
    flattenDepth<T>(array: ListOfRecursiveArraysOrValues<T>, depth?: number): T[];
  }
}
3. 降级库版本(不推荐)
npm install lodash@4.14.0  # 与类型包匹配

全局类型污染

场景:类型包声明了全局类型,影响了我们的代码。例如:我们在使用 @types/jest,它声明了全局的 describeitexpect 。但在我们的非测试代码中,这些是不应该存在的。

解决方案

1. 隔离测试类型
// 在 tsconfig.json 中
{
  "extends": "./tsconfig.base.json",
  // 主配置:排除测试类型
  "compilerOptions": {
    "types": []  // 不包含任何全局类型
  },
  
  // 测试配置
  "tsconfig.test.json": {
    "extends": "./tsconfig.json",
    "compilerOptions": {
      "types": ["jest", "node"]  // 只在测试中包含
    }
  }
}
2. 使用import代替全局
import { describe, it, expect } from '@jest/globals';

describe('test', () => {
  it('should work', () => {
    expect(1 + 1).toBe(2);
  });
});

循环依赖的类型包

现象:类型包相互依赖导致循环。例如:@types/react 依赖于 @types/prop-types;而 @types/prop-types 反过来又可能间接引用 @types/react 。结果就导致:导入循环或最大深度超出。

解决方案

1. 使用 import type

即:在类型包中,只导入类型,不导入值:

import type { ReactNode } from 'react';
2. 使用三斜线引用
/// <reference types="react" /> // 这告诉TypeScript类型存在,但不导入

在 ES6 以前,JavaScript 并没有一个官方的模块系统,而是在不同的环境中,以不同的方式加上了这个缺失的特性。如 node.js 使用 requiremodule.exports;AMD 使用一个带回调的 define 函数。后来,TypeScript 通过 module 关键字和“三斜线”导入来填补了这个空白。

module 关键字和“三斜线”导入只是一个历史遗迹,在 ES6+ 中,还是推荐 importexport

3. 重新设计类型结构

即:将共享类型提取到单独的文件:

// types/shared/index.d.ts
export interface CommonProps {
  className?: string;
  style?: React.CSSProperties;
}

// types/react/index.d.ts  
import { CommonProps } from '../shared';

// types/vue/index.d.ts
import { CommonProps } from '../shared';

最佳实践指南

作为类型包使用者

package.json 最佳配置

// package.json 最佳配置
{
  "name": "my-app",
  "version": "1.0.0",
  "devDependencies": {
    // 1. 匹配运行时版本
    "@types/node": "^18.0.0",  // 与Node.js版本匹配
    
    // 2. 使用^允许次要版本更新
    "@types/react": "^18.0.0",
    "@types/react-dom": "^18.0.0",
    
    // 3. 按需安装,不要一次性安装所有@types
    // 错误做法:@types/* (安装所有)
    // 正确做法:只安装需要的
    
    // 4. 定期更新
    // npm update @types/*
  },
  "scripts": {
    // 5. 添加类型检查脚本
    "type-check": "tsc --noEmit",
    "type-check:watch": "tsc --noEmit --watch"
  }
}

tsconfig.json 最佳配置

// tsconfig.json 最佳实践
{
  "compilerOptions": {
    // 1. 明确指定类型包
    "types": [
      "node",
      "react",
      "react-dom"
      // 明确列出,避免意外包含
    ],
    
    // 2. 控制类型查找路径
    "typeRoots": [
      "./node_modules/@types",
      "./src/types"  // 自定义类型目录
    ],
    
    // 3. 启用严格检查
    "strict": true,
    
    // 4. 处理模块解析
    "moduleResolution": "node",
    
    // 5. 确保兼容性
    "skipLibCheck": true,  // 跳过库的类型检查,提高编译速度
    "forceConsistentCasingInFileNames": true
  },
  
  // 6. 包含和排除文件
  "include": [
    "src/**/*",
    "tests/**/*"
  ],
  "exclude": [
    "node_modules",
    "dist",
    "**/*.test.ts",
    "**/*.spec.ts"
  ]
}

作为类型包贡献者

1. 使用函数重载而不是联合类型

// ❌ 不好
function parse(input: string | number): Date;

// ✅ 更好
function parse(input: string): Date;
function parse(input: number): Date;

2. 使用泛型提高灵活性

// ❌ 不灵活、不安全
function getValue(obj: any, key: string): any;

// ✅ 类型安全
function getValue<T, K extends keyof T>(obj: T, key: K): T[K];

3. 提供完整的JSDoc注释

/**
 * 验证电子邮件地址
 * @param email - 要验证的电子邮件地址
 * @param strict - 是否进行严格验证
 * @returns 验证结果
 * @example
 * validateEmail('test@example.com') // true
 */
export function validateEmail(email: string, strict?: boolean): boolean;

4. 包含类型测试

import _ = require('lodash');

// 测试类型推断
const numbers: number[] = [1, 2, 3];
const doubled = _.map(numbers, x => x * 2);  // 应该推断为 number[]

// 测试边界情况
_.chunk([], 2);  // 应该返回 [] 而不是 never[]

5. 遵循命名约定

// 使用帕斯卡命名法(PascalCase)表示类型和接口
interface UserConfig { /* ... */ }
type ValidationResult = { /* ... */ };

// 使用驼峰命名法(camelCase)表示函数和变量
function validateInput(input: string): boolean;
const defaultConfig: UserConfig = { /* ... */ };

TypeScript的演进

TypeScript正在减少对@types的依赖

  1. 声明文件自动生成:使用 tsc --declaration 自动生成 .d.ts 文件
  2. 更好的模块解析:TypeScript 4.7+ 改进了对 package.jsonexports 的支持
  3. 类型导入优化:import type 语法,减少运行时依赖
  4. 部分类型检查:可以对 JavaScript 文件进行渐进式类型检查

开发者建议

库开发者:

  1. 用TypeScript编写你的库
  2. 发布时包含类型定义
  3. 使用严格类型检查
  4. 提供完整的API文档

应用开发者:

  1. 优先选择有自带类型的库
  2. 定期更新@types包
  3. 学会阅读和理解类型定义
  4. 为缺失类型的库贡献类型定义

类型维护者:

  1. 保持与上游库的同步
  2. 编写全面的类型测试
  3. 及时响应社区问题
  4. 遵循DefinitelyTyped的贡献指南

结语

@types 生态系统是 TypeScript 成功的关键因素之一。通过 DefinitelyTyped 项目,社区为成千上万的 JavaScript 库提供了高质量的类型定义。对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!

7.BTC-挖矿难度-北大肖臻老师客堂笔记

作者 前端涂涂
2026年1月26日 15:34

第 7 讲(P7)的核心内容是围绕比特币的挖矿难度调整以及以太坊中针对分叉问题的解决方案(GHOST 协议)

以下是该课程内容的结构化总结:

一、 为什么要调整挖矿难度?

为了维持系统的稳定性。比特币规定平均每 10 分钟产生一个区块。

  • 如果不调整: 随着计算机算力(哈希率)的提升,出块时间会越来越短。这会导致区块链频繁分叉,不仅降低系统安全性(容易受到攻击),还会造成大量的计算资源浪费。

二、 比特币的难度调整机制

  1. 调整周期: 每隔 2016 个区块(约 2 周时间)调整一次难度。
  2. 调整公式:
  • 目标值(Target)决定了难度,Target 越小,难度越大。
  • 公式:New Target = Old Target × (实际产生2016个区块的时间 / 预期时间2周)
  1. 限制保护: 为了防止波动过大,单次难度调整的最大幅度限制在 4 倍以内(即难度最多增加到原来的 4 倍,或减少到原来的 1/4)。

三、 相关核心概念(Orphan, Ghost, Uncle)

随着出块速度的加快(如以太坊约 15 秒一区块),分叉会变得非常频繁。为了处理这些分叉,引入了以下概念:

1. Orphan Block(孤块)

  • 定义: 在比特币中,如果两个矿工几乎同时挖出区块,只有一条链会成为“最长合法链”,另一条链上的区块被称为“孤块”。
  • 结果: 在比特币中,孤块是完全无效的,矿工拿不到任何奖励。这对于算力较小的个体矿工不公平。

2. Uncle Block(叔父块)与 Uncle Reward(叔父奖)

这是以太坊为了解决孤块问题引入的机制:

  • Uncle Block: 虽然没能进入主链,但其“父母”是主链上的区块(即曾经发生过分叉但败北的区块)。
  • Uncle Reward(奖励): 为了鼓励矿工并提高系统安全性,以太坊会给这些叔父块的矿工一定的奖励(通常是区块奖励的 7/8 左右)。
  • 作用: 减少了大型矿池因为网络延迟优势对小矿工的剥削,使系统更加去中心化。

3. GHOST 协议

  • 全称: Greedy Heaviest Observed Subtree(观察到的最重子树协议)。
  • 核心思想: 在决定哪条是“主链”时,不简单地看哪条链最长,而是看哪条链包含的**工作量(包含的区块总数,包括叔父块)**最多。
  • 目的: 即使出块时间很短(分叉多),也能通过计入分叉块的工作量,快速使全网达成共识,防止 51% 攻击。

四、 总结:核心逻辑链

  1. 算力增长 出块变快 调整难度(维持 10 分钟/块)。
  2. 出块太快(如以太坊) 产生大量 Orphan Block(浪费且不安全)。
  3. 引入 GHOST 协议 将孤块变为 Uncle Block 并给予 Uncle Reward
  4. 最终目的 既能保持快速确认(高 TPS),又能保证系统的公平性与安全性。

在这里插入图片描述转存失败,建议直接上传图片文件

你的电脑,值得一次专业“深度清洁”:告别临时文件,清理重复与相似内容

2026年1月26日 15:33

Czkawka/Krokiet:基于 Rust 的跨平台系统清理工具深度技术解析

1. 整体介绍

1.1 项目概况

项目地址github.com/qarmin/czka…
当前状态:截至分析时,该项目在 GitHub 上已获得超过 3万 star 和 近千 fork,显示出较高的社区关注度和实用性。项目采用 Rust 编写,遵循内存安全理念,是一个活跃维护的开源项目。

项目演进:项目最初以 Czkawka(GTK4 GUI)为核心,现已演进为以 Krokiet(Slint GUI)为新一代前端。Czkawka GTK 版本进入维护模式,仅接收错误修复,而 Krokiet 则处于积极开发阶段,并新增了多项功能。

1.2 主要功能与界面

该项目本质上是一个多功能磁盘空间清理与文件管理工具集。其核心价值在于通过多种专用扫描器,精准定位并帮助用户清理计算机中的冗余、无效或潜在问题文件。

核心功能矩阵

功能类别 具体工具 解决的问题
重复清理 重复文件、相似图片、相似视频、相同音乐 消除内容重复造成的空间浪费
空间回收 空文件夹、空文件、大文件、临时文件 直接删除无内容或占用空间大的文件
系统维护 无效符号链接、损坏文件、错误扩展名文件 修复或清理可能影响系统稳定性的问题文件
隐私与优化 Exif 移除器、视频优化器、不良文件名 移除隐私元数据、优化媒体文件体积、规范文件名

界面截图示意(基于 README 描述): 在这里插入图片描述

  • Krokiet (Slint UI): 界面现代化,功能区划清晰,支持新增的 Exif 清理、视频优化等操作面板。

在这里插入图片描述

  • Czkawka (GTK4 UI): 经典桌面应用布局,工具以标签页形式呈现。

1.3 面临问题与目标人群

解决问题

  1. 磁盘空间无序占用:用户难以手动全面查找重复文件、空文件夹、缓存文件等“隐形”空间占用者。
  2. 文件管理效率低下:缺乏批量、智能识别相似或问题文件的工具(如不同分辨率的同一图片、损坏的文档)。
  3. 跨平台工具缺失:许多优秀清理工具仅限特定平台(如仅限 Linux 的 FSlint)。
  4. 隐私泄露风险:图片中的 Exif 数据、临时文件可能包含敏感信息,普通用户缺乏便捷清理手段。
  5. 现有方案不足:同类工具如 BleachBit 侧重临时文件清理,DupeGuru 侧重重复查找,功能单一。

目标人群

  • 普通桌面用户:希望便捷、安全地释放磁盘空间。
  • 摄影与多媒体爱好者:需要管理大量相似图片、视频,或清理媒体文件元数据。
  • 开发与运维人员:需要命令行工具进行自动化清理,或集成清理功能到其他应用中。
  • 跨平台用户:在 Windows, macOS, Linux 等多系统环境下均需使用统一工具。

1.4 解决方案与优势

传统解决方式

  • 组合使用多个单功能工具(如 fdupes + rmlint + 手动查找)。
  • 使用功能全面但可能较臃肿、非跨平台或已停止维护的工具(如 FSlint)。
  • 手动编写脚本,但鲁棒性差,难以处理复杂场景(如相似图像比对)。

Czkawka/Krokiet 新方案优势

  1. 功能聚合:将14类清理工具集成于一体,提供统一入口和操作逻辑。
  2. 技术栈先进
    • 语言:采用 Rust,保障内存安全与高性能,编译为单一可执行文件,部署简单。
    • 架构:核心逻辑 (czkawka_core) 与前端展示 (GUI/CLI) 分离,利于复用和生态扩展。
    • 并行化:广泛使用 rayon 等库进行并行遍历和计算,充分利用多核CPU。
  3. 用户体验优化
    • 缓存机制:首次扫描后建立缓存,大幅提升后续扫描速度。
    • 无损操作:默认仅查找和展示,删除等危险操作需用户二次确认,支持先移动到回收站。
    • 多前端:同时提供图形界面(Slint/GTK)和命令行界面,满足不同场景需求。

1.5 商业价值与生态潜力评估

价值估算逻辑

  1. 代码开发成本估算:项目包含约数万行 Rust 代码,涉及文件系统、多媒体解析、哈希算法、GUI 框架集成等多个复杂领域。若以商业团队开发,人力成本相当可观。其开源性质使得社区可以零成本获得该能力。
  2. 覆盖问题空间效益
    • 直接效益:帮助用户高效回收磁盘空间,对于使用 SSD 或存储空间紧张的用户而言,等同于延长硬件使用寿命或推迟升级投入。
    • 间接效益:通过清理损坏文件、无效链接,可能预防由文件系统错误引发的系统不稳定,减少维护时间。
    • 隐私效益:提供便捷的元数据清理工具,降低隐私泄露风险,其价值难以量化但确实存在。

生态潜力

  • 核心库 (czkawka_core) 已被其他项目(如 Tauri 前端、文档校正库)作为依赖复用,证明了其代码质量和模块化设计的价值。
  • 作为 Rust 在桌面工具开发中的一个成功案例,对推广 Rust 生态有积极作用。
  • 项目接受捐赠,形成了初步的“开源-捐赠”可持续循环雏形。

2. 详细功能拆解(产品+技术视角)

2.1 核心功能模块

项目功能可归纳为四大模块,每个模块包含若干技术驱动的工具:

模块 包含工具 技术实现关键点
重复内容识别 重复文件、相似图片、相似视频、相同音乐 分层哈希(大小、哈希)、感知哈希(pHash)、音频特征提取、多线程比对
空间占用分析 大文件、空文件、空文件夹、临时文件 递归目录遍历、文件元数据快速读取、基于规则的路径/扩展名匹配
文件系统完整性 无效符号链接、损坏文件、错误扩展名 链接目标存在性检查、文件头魔法字节验证、内容与扩展名匹配
文件内容优化 Exif移除器、视频优化器、不良文件名 图像元数据操作、调用外部工具(如ffmpeg)转码、文件名编码与字符集检查

2.2 技术支撑要点

  1. 跨平台文件系统操作:通过 Rust 标准库 std::fsstd::path 实现基础操作,并利用 trash crate 实现跨平台的“移到回收站”功能,提升安全性。
  2. 高性能目录遍历:在 czkawka_core::common::dir_traversal 中实现自定义的并行遍历器,优于简单的递归,并能集成进度回调。
  3. 缓存设计:扫描结果(如文件哈希)可序列化保存到磁盘,下次扫描时通过缓存快速跳过未变更的文件,其逻辑位于 czkawka_core::common::cache
  4. 外部工具集成:视频优化依赖于 ffmpeg,通过 czkawka_core::common::ffmpeg_utils 封装调用逻辑,处理跨平台路径和参数。

3. 技术难点分析

  1. 性能与精度的平衡

    • 难点:全盘扫描数十万文件时,逐字节计算哈希(如 SHA256)虽精确但极慢;仅用文件名和大小又容易误判。
    • 解决方案:采用分层哈希策略。先比较文件大小,快速过滤;大小相同者计算快速哈希(如 XXH3);快速哈希相同者,再计算强加密哈希(如 Blake3)确认。此逻辑体现在 duplicate 工具中。
  2. 相似性判定的复杂度

    • 难点:判断“相似”图片/视频比判断“相同”更复杂,需抵抗分辨率变化、水印、亮度调整等。
    • 解决方案:使用感知哈希(Perceptual Hash)。对于图片,将图像缩放到固定大小,转化为灰度图,计算离散余弦变换(DCT)并比较频域特征。这通过 image_hasher 库实现。
  3. 跨平台 GUI 的挑战

    • 难点:GTK4 在 Windows/macOS 上原生体验和分发便利性不足。
    • 解决方案:引入 Slint 作为 Krokiet 的 GUI 框架。Slint 使用声明式 UI 语言,可编译为原生代码,能较好地平衡性能、外观和跨平台一致性。从 krokiet/src/main.rs 可见其与 Rust 模型的深度绑定。
  4. 原子性文件操作

    • 难点:创建硬链接或符号链接时,如果目标已存在,需要原子性地替换,避免在操作过程中留下损坏状态或丢失原文件。
    • 解决方案:在 common/mod.rsmake_hard_linkmake_file_symlink 函数中,采用“创建临时文件 -> 重命名原文件 -> 创建链接 -> 删除临时文件”的策略。若链接创建失败,则回滚重命名操作,保证原文件安全。

4. 详细设计图

4.1 系统架构图

在这里插入图片描述

架构解读:这是一个典型的分层与模块化架构czkawka_core 作为核心库,封装了所有业务逻辑和数据模型。不同前端通过调用核心库的公共 API 来工作。核心库内部,tools 模块实现具体功能,common 模块提供共享设施。这种设计实现了前端与后端的解耦,也是 czkawka_core 能被其他项目复用的基础。

4.2 核心扫描链路序列图

sequenceDiagram
    participant U as 用户
    participant GUI as Krokiet GUI
    participant CM as 核心模型
    participant DT as 目录遍历器
    participant TK as 特定工具逻辑
    participant Cache as 缓存系统

    U->>GUI: 点击“扫描”按钮
    GUI->>CM: 初始化扫描任务 (设置路径、参数)
    CM->>Cache: 加载已有缓存
    CM->>DT: 启动并行目录遍历
    loop 遍历每个文件/目录
        DT->>TK: 交付文件项
        TK->>Cache: 检查是否有有效缓存
        alt 缓存命中
            Cache-->>TK: 返回缓存结果
        else 缓存未命中
            TK->>TK: 执行计算 (如计算哈希)
            TK->>Cache: 存储新结果
        end
        TK-->>CM: 返回单项结果
        CM-->>GUI: 推送进度 & 增量结果
    end
    CM->>GUI: 通知扫描完成
    GUI->>U: 展示结果列表

流程解读:此序列图展示了从用户操作到结果展示的核心数据流。关键点在于缓存集成增量结果推送。遍历器 (DT) 与具体工具 (TK) 协同工作,缓存检查贯穿始终,避免了重复计算。进度和结果被实时推送到 GUI,实现了用户界面在扫描过程中的响应式更新。

4.3 核心工具类关系图

classDiagram
    class ProgressData {
        +current_stage: String
        +files_checked: u64
        +files_to_check: u64
        +update_progress()
    }

    class DirTraversalBuilder {
        +roots: Vec<PathBuf>
        +group_by: GroupByOption
        +build() -> DirTraversal
    }

    class DirTraversal {
        -stop_receiver: Receiver<bool>
        +run(progress_sender: Sender<ProgressData>)
    }

    class ToolTrait {
        <<interface>>
        +find_duplicates(&mut self, ...)
        +get_stop_receiver(&self) -> Receiver<bool>
    }

    class DuplicateFinder {
        -hash_type: HashType
        -cache: Arc<Cache>
        +find_duplicates()
    }

    class SimilarImageFinder {
        -hash_alg: HashAlg
        -max_size: u64
        +find_similar_images()
    }

    ProgressData <.. DirTraversal : 发送
    DirTraversalBuilder *--> DirTraversal : 构建
    ToolTrait <|.. DuplicateFinder : 实现
    ToolTrait <|.. SimilarImageFinder : 实现
    DirTraversal ..> ToolTrait : 调用(通过回调)

类图解读ProgressData 是贯穿全局的进度信息载体。DirTraversalBuilder 采用建造者模式,灵活配置遍历参数并生成 DirTraversal 执行器。所有具体工具(如 DuplicateFinder, SimilarImageFinder)都实现一个公共的 ToolTrait(在代码中为 tools 模块各文件中的结构体和方法),这保证了它们可以被统一的扫描流程驱动。DirTraversal 在执行时会调用这些工具提供的回调函数处理每个文件项。

4.4 核心函数 make_hard_link 操作流图

在这里插入图片描述

流程图解读:此图详细说明了 make_hard_link 函数为了保证原子性和安全性所采取的“重命名-创建-清理”三步法。其核心思想是:在修改目标 (dst) 之前,先将其移动到一个临时备份位置 (temp)。如果新链接创建成功,则删除备份;如果创建失败,则将备份移动回原处,恢复原状。这个过程确保了在任何情况下,dst 路径指向的文件(无论是旧的用户文件还是新创建的硬链接)都是完整可用的,不会出现路径悬空或文件丢失。

5. 核心代码解析

以下选取 czkawka_core/src/common/mod.rs 中的 make_hard_link 函数进行深度解析,它集中体现了项目对文件系统操作安全性和跨平台鲁棒性的考量。

/// 创建一个硬链接,如果目标文件已存在,则原子性地替换它。
/// 这是安全的,因为即使在操作过程中程序崩溃,原文件也会被保留或恢复。
pub fn make_hard_link<P: AsRef<Path>, Q: AsRef<Path>>(src: P, dst: Q) -> io::Result<()> {
    let src = src.as_ref();
    let dst = dst.as_ref();
    // 1. 获取目标文件的父目录,用于存放临时文件
    let dst_dir = dst.parent().ok_or_else(|| Error::other("No parent"))?;

    let mut temp;
    let mut attempts = MAX_SYMLINK_HARDLINK_ATTEMPTS; // 最大尝试次数,默认为5
    // 2. 循环生成一个不存在的临时文件名
    loop {
        temp = dst_dir.join(format!("{}.czkawka_tmp", rand::random::<u128>()));
        if !temp.exists() {
            break;
        }
        attempts -= 1;
        if attempts == 0 {
            return Err(Error::other("Cannot choose temporary file for hardlink creation"));
        }
    }
    // 3. 关键步骤:将目标文件原子性地重命名为临时文件
    //    此时,`dst` 路径不再指向任何文件。
    fs::rename(dst, temp.as_path())?;

    // 4. 尝试创建从 src 到 dst 的硬链接
    match fs::hard_link(src, dst) {
        Ok(()) => {
            // 5. 创建成功:删除旧的临时文件(即原文件)
            fs::remove_file(&temp)?;
            Ok(())
        }
        Err(e) => {
            // 6. 创建失败:将临时文件(原文件)重命名回 dst,进行回滚
            let _ = fs::rename(&temp, dst);
            Err(e)
        }
    }
}

代码关键点解析

  1. 原子性替换逻辑 (第3-6步):这是函数的核心。直接删除 dst 再创建链接是危险的,因为删除后、创建前系统若崩溃,文件将丢失。本函数采用“重命名原文件 -> 创建链接 -> 删除原文件”的顺序,保证了 dst 路径在任何时刻都指向一个有效文件。
  2. 临时文件命名 (第2步):使用 rand::random::<u128>() 生成一个全局唯一标识符,极大降低了与现有文件重名的概率。循环和尝试次数限制 (MAX_SYMLINK_HARDLINK_ATTEMPTS) 提供了额外的鲁棒性。
  3. 错误恢复 (第6步):如果 fs::hard_link 失败(例如源文件不存在、跨设备链接等),函数会尝试将临时文件重命名回原始位置 (dst)。let _ = ... 表示忽略回滚操作的错误,因为此时首要任务是返回硬链接创建失败的原因。
  4. 跨平台性:该函数完全基于 Rust 标准库 std::fs,其 hard_linkrename 操作在主流操作系统上均有良好定义和支持,确保了跨平台行为的一致性。

为何重要:此函数虽小,但体现了系统工具软件的基石思想——数据安全第一。它被用于重复文件清理中的“创建硬链接以合并重复项”功能,确保用户数据即使在工具执行中出现意外时也不会受损。类似的谨慎逻辑也体现在 make_file_symlink(处理软链接)和文件删除(先移至回收站)等操作中,共同构成了项目可靠性的基础。


总结:Czkawka/Krokiet 项目展示了一个成功的开源工具应具备的特质:解决明确痛点、采用恰当技术、架构清晰可扩展、注重用户体验与数据安全。它不仅是 Rust 在桌面应用领域的一个有力例证,其模块化设计(特别是 czkawka_core)也为构建更复杂的文件管理生态系统提供了可能。对于开发者而言,该项目是学习 Rust 系统编程、跨平台 GUI 设计和高性能并发算法的优质参考。

React记录之useReducer

作者 web_bee
2026年1月26日 15:17

useReducer 是 React 提供的一个内置 Hook,用于在函数组件中管理复杂的状态逻辑。它类似于 Redux 的简化版,适用于状态更新逻辑较复杂、多个子值依赖彼此、或需要可预测状态变化的场景。

一、基本使用方法

1. 语法

const [state, dispatch] = useReducer(reducer, initialState, init);
  • reducer:一个纯函数,接收当前 state 和 action,返回新的 state。
  • initialState:初始状态。
  • init(可选) :用于惰性初始化的函数,initialState = init(initialArg)

2. 示例

"use client"

import React, { useReducer } from 'react';


type actionType = {
  type: string;
  value?: any;
}

const initialState = {
  count: 0,
  name: 'Reducer Example',
};
const reducer = (state: any, action: actionType) => {
  switch (action.type) {
    case 'count':
      return { ...state, count: action.value };
    case 'name':
      return { ...state, name: action.value };
    default:
      return state;
  }
};

const ReducerPage = () => {

  const [state, dispatch] = useReducer(reducer, initialState);

  return <div>
    <h1>{state.name}</h1>
    <p>Count: {state.count}</p>
    <button onClick={() => dispatch({ type: 'count', value: state.count + 1 })}>Increment Count</button>
    <button
      onClick={() => dispatch({
        type: 'name',
        value: `Updated Reducer Example: ${new Date().toLocaleTimeString()}`
      })}
    >
      Change Name
    </button>
  </div>;
};

export default ReducerPage;

二、使用场景

1. 状态逻辑复杂

useState 难以维护(例如状态包含多个子属性、更新逻辑相互依赖)时,useReducer 更清晰。

2. 多个组件共享状态更新逻辑

配合 useContext 可实现“类 Redux” 的全局状态管理,避免 props drilling。

3. 需要可预测、可测试的状态转换

reducer 是纯函数,便于单元测试;所有状态变更通过 dispatch(action) 触发,便于追踪。

4. 表单状态管理

如多步骤表单、动态字段增删等,用 useReducer 可集中处理字段变更、验证、重置等逻辑。

三、注意事项

1. reducer 必须是纯函数

  • 不能有副作用(如 API 调用、直接修改 state)。
  • 相同输入必须返回相同输出。

2. 不要直接修改 state

始终返回新对象,而不是修改原 state:

// ❌ 错误
state.count++;
return state;

// ✅ 正确
return { ...state, count: state.count + 1 };

3. 性能优化

  • 如果 reducer 计算开销大,可考虑 useMemo 缓存中间结果(但 reducer 本身通常很快)。
  • dispatch 函数在组件 re-render 期间是稳定的(不会变),可安全用于依赖数组(如 useEffect)。

4. 与 useState 的选择

  • 简单状态 → useState
  • 复杂状态逻辑、多个相关状态、需要集中管理 → useReducer

5. 调试支持

可通过在 reducer 中加日志,或使用 React DevTools 查看 dispatch 的 action。

四、进阶:惰性初始化(Lazy Initialization)

如果初始状态计算开销大,可用第三个参数 init

const init = (initialCount) => ({ count: initialCount });

function reducer(state, action) { /* ... */ }

function Counter({ initialCount }) {
  const [state, dispatch] = useReducer(reducer, initialCount, init);
}

这样 init 只在首次渲染时执行一次。

总结

特性 useState useReducer
适用场景 简单独立状态 复杂、关联状态
状态更新 直接设值或函数 通过 dispatch(action)
可读性 简单场景更直观 复杂逻辑更清晰
测试性 较弱 强(纯函数)

合理使用 useReducer 能让状态管理更健壮、可维护。

❌
❌