阅读视图

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

面试官:JWT、Cookie、Session、Token有什么区别?

JWT、Cookie、Session、Token 是 Web 开发中常用的身份认证和状态管理技术,它们之间既有区别,也有联系

一、JWT(JSON Web Token)

JWT 是一种开放标准(RFC 7519),用于在网络应用之间安全地传输信息(通常是身份认证信息)。它是一个自包含的、可验证的、不可篡改的字符串,格式如下:

Header.Payload.Signature

三部分组成:

  1. Header(头部):声明类型和签名算法(如 HS256)。
  2. Payload(载荷):包含用户信息(如用户 ID、角色等)和元数据(如过期时间)。
  3. Signature(签名):用密钥对 Header 和 Payload 签名,防止篡改。

特点:

  • 无需服务器存储(无状态)。
  • 可跨域使用(常用于分布式系统、微服务)。
  • 一旦签发,在过期前无法撤销(除非引入黑名单机制)。

二、Cookie

Cookie 是浏览器存储的一小段文本信息,由服务器通过 HTTP 响应头 Set-Cookie 设置,浏览器在后续请求中自动携带。

特点:

  • 自动携带(浏览器行为)。
  • 可设置过期时间、作用域、HttpOnly、Secure 等属性。
  • 容量小(约 4KB)。
  • 可用于存储 Session ID 或 JWT。

三、Session(会话)

Session 是服务器端维护的用户会话状态。通常流程如下:

  1. 用户登录后,服务器创建一个 Session,生成一个唯一的 Session ID
  2. Session ID 通过 Cookie 返回给浏览器。
  3. 浏览器后续请求自动携带该 Cookie,服务器通过 Session ID 查找对应的用户状态。

特点:

  • 状态存储在服务器端(通常是内存、Redis、数据库)。
  • 安全性较高(用户无法直接篡改)。
  • 不适合分布式系统(需要共享 Session 存储)。

四、Token(令牌)

Token 是一个广义概念,指用于身份验证的凭证。JWT 就是一种 Token。

常见 Token 类型:

  • Access Token(访问令牌):用于访问资源。
  • Refresh Token(刷新令牌):用于获取新的 Access Token。
  • JWT:一种结构化的 Token。

五、它们之间的关系与区别

名称 存储位置 状态管理 安全性 适用场景
JWT 客户端 无状态 分布式系统、移动端、API 认证
Cookie 客户端 无状态 存储小量数据、自动携带
Session 服务器端 有状态 传统 Web 应用
Token 客户端 无状态 通用身份凭证(JWT 是其一)

六、常见组合方式

方式一:Session + Cookie(传统 Web)

  • 登录后服务器创建 Session,Session ID 存 Cookie。
  • 每次请求带 Cookie,服务器查 Session 验证身份。

方式二:JWT + Header(前后端分离)

  • 登录后服务器返回 JWT,前端存 localStorage 或 Cookie。
  • 每次请求手动在 Header 中加 Authorization: Bearer <JWT>

方式三:JWT + Cookie(安全增强)

  • JWT 存 Cookie,设置 HttpOnly + Secure,防止 XSS。
  • 浏览器自动携带,服务器解析 JWT 验证身份。

七、总结

  • JWT 是一种自包含的 Token不依赖服务器存储
  • Cookie浏览器存储机制,可存 Session ID 或 JWT。
  • Session服务器存储的用户状态,依赖 Cookie 传递 ID。
  • Token身份凭证,JWT 是其中一种实现。

前端画布类型编辑器项目,历史记录技术方案调研

image.png

image.png

最近在做一个在线PPT编辑器,其中状态管理用到了Zustand,撤销重做功能用的是zundo,编辑器类型的项目一般都会有历史记录功能,但是记录用户的操作有哪些方案呐?

主流技术方案对比

目前,业界主要有三种实现思路:命令模式状态快照(备忘录模式)和差异-补丁(Diff-Patch)

方案一:命令模式 (Command Pattern)

这是实现撤销/重做功能最经典、最正统的设计模式。

  • 核心思想:

    将用户的每一个“操作”封装成一个包含execute(执行)和undo(撤销)方法的命令对象。

  • 数据结构:

    使用两个栈:undoStack(撤销栈)和redoStack(重做栈)。

  • 工作流程:

    1. 执行操作: 用户执行一个新操作(如“添加矩形”)。
    2. 创建一个AddRectangleCommand对象。
    3. 调用command.execute()来执行该操作(更新画布状态)。
    4. 将该command压入undoStack
    5. 撤销 (Undo):
    6. undoStack弹出一个command
    7. 调用command.undo()
    8. 将该command压入redoStack
    9. 重做 (Redo):
    10. redoStack弹出一个command
    11. 调用command.execute()
    12. 将该command压入undoStack
  • 优点:

    • 内存占用低: 只存储“操作”本身,而不是完整的画布状态。
    • 逻辑清晰: undoexecute逻辑高度内聚,易于理解和测试。
    • 高性能: undo/redo操作通常很快,因为它们只执行逆向/正向操作。
  • 缺点:

    • 实现复杂度高: 必须为每一个可撤销的操作(移动、缩放、变色、删除、组合...)编写一个具体的Command类及其undo逻辑。
    • undo的实现难度: undo操作(如“撤销删除”)可能需要存储被删除对象的状态,这会增加命令对象的复杂度。

方案二:状态快照 (State Snapshots / Memento Pattern)

这是一种实现上更简单粗暴,但在特定场景下非常有效的模式。

  • 核心思想:

    在每次“有效操作”结束时,将整个画布的完整状态(通常是JSON数据)序列化并存储起来。

  • 数据结构:

    一个数组(historyStack)和一个指针(currentIndex)。

  • 工作流程:

    1. 执行操作: 用户完成操作(如拖拽结束)。
    2. 获取当前画布的完整状态newState
    3. newState添加到historyStackcurrentIndex的位置。
    4. currentIndex加一。
    5. 撤销 (Undo):
    6. currentIndex减一。
    7. historyStack[currentIndex]获取previousState
    8. previousState完全覆盖当前画布状态并重新渲染。
    9. 重做 (Redo):
    10. currentIndex加一。
    11. historyStack[currentIndex]获取nextState
    12. nextState覆盖当前状态并重新渲染。
  • 优点:

    • 实现简单: 核心逻辑与业务操作解耦。历史记录系统不需要“理解”什么是“移动”,什么是“变色”,它只负责保存和恢复状态。
    • 绝对可靠: 只要状态的序列化和反序列化是正确的,undo/redo就绝对不会出错。
  • 缺点:

    • 内存占用极大: 如果画布状态有10MB,100步历史就是1GB内存。这在Web端几乎是不可接受的。
    • 性能瓶颈: 序列化/反序列化/深拷贝大型状态对象(Deep Clone)可能非常耗时,导致UI卡顿。

方案三:增量差异 (Diff-Patch)

这是快照模式的进一步演进,也是目前在React等状态驱动框架中非常流行的一种方案。

  • 核心思想:

    结合了命令模式(只存变化)和快照模式(不关心操作逻辑)的优点。它不存储完整的状态,也不存储操作命令,而是存储两个状态之间的差异(Diff/Patch)。

  • 数据结构:

    undoStack(存储逆向Patch)和redoStack(存储正向Patch)。

  • 工作流程:

    1. 执行操作:
    2. (操作前)记录当前状态 State A
    3. (操作后)生成新状态 State B
    4. 计算差异: Patch (A -> B)(正向补丁)和 Inverse Patch (B -> A)(逆向补丁)。
    5. Inverse Patch压入undoStack
    6. Patch压入redoStack(或在undo时再计算)。
    7. 撤销 (Undo):
    8. undoStack弹出Inverse Patch
    9. 将该补丁Apply到当前状态,使其回退到State A
    10. 重做 (Redo):
    11. redoStack弹出Patch
    12. 将该补丁应用到当前状态,使其前进到State B
  • 优点:

    • 内存与性能均衡: 内存占用远小于全量快照(只存Diff),实现复杂度远低于命令模式(自动生成Diff和Patch)
  • 缺点:

    • Diff/Patch的开销: 如果一次操作改变了状态树的很多部分,计算Diff和生成Patch本身也可能有性能开销(但通常快于深拷贝)。
    • 依赖库: 通常需要依赖一个健壮的Diff/Patch库

方案对比总结

特性 命令模式 (Command Pattern) 状态快照 (State Snapshot) 增量差异 (Diff-Patch)
核心 存储“操作” 存储“完整状态” 存储“状态差异”
内存占用 极低 极高 较低
性能开销 undo/redo极快 存取时开销大 (深拷贝/序列化) 存取时有Diff/Patch计算开销
实现复杂度 极高 (需实现所有undo逻辑) 极低 中等 (需依赖Diff库)
适用场景 性能和内存要求苛刻的复杂应用 状态简单的小型应用 现代前端框架 (React/Vue) ,状态驱动型应用

⚡️ Zustand 撤销重做利器:Zundo 实现原理深度解析

image.png

image.png

最近在做一个在线PPT编辑器,其中状态管理用到了Zustand,撤销重做功能用的是zundo,好奇是如何实现的。于是看看源码,写篇文章总结一下。

"撤销/重做"(Undo/Redo)功能是提升用户体验不可或缺的一环。对于使用Zustand这款轻量级状态管理库的开发者来说,zundo 提供了一个极其优雅和简单的解决方案。

zundo 是一个专门为 Zustand 设计的中间件,它几乎以"零配置"的方式为Zustand的状态管理添加了强大的历史记录功能。

如何在项目中使用 Zundo

1. 安装

npm install zundo
# 或者
yarn add zundo

2. 封装Store

只需要将 create 函数(Zustand store 的定义)用 zundo 中间件包裹起来即可

假设有一个简单的计数器 store:

import create from 'zustand';

// 你的 store 定义
const createStore = (set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
});

要为其添加 undo/redo 功能,只需这样做:

import { create } from 'zustand';
import { temporal } from 'zundo';

// 你的 store 定义
const createStore = (set, get) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  // ... 其他 actions
});

// 使用temporal封装
export const useStore = create(temporal(createStore));

3. 在组件中使用

zundo 会自动向你的 store 中注入 undoredoclear 等 action,以及 pastStatesfutureStates 两个状态数组

function App() {
  // 从 store 中获取状态和 actions
  const { count, increment, decrement } = useStore();

  // 从 zundo 获取 undo/redo
  const { undo, redo, clear, pastStates, futureStates } = useStore.temporal.getState();

  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>

      <hr />
      <button onClick={undo} disabled={pastStates.length === 0}>
        Undo (撤销)
      </button>
      <button onClick={redo} disabled={futureStates.length === 0}>
        Redo (重做)
      </button>
      <button onClick={clear}>Clear History (清除历史)</button>
    </div>
  );
}

实现原理

  • create 时通过一个 mutator 把独立的时间旅行子 store 注入到主 store 的 store.temporal 上。
  • 拦截所有状态更新(外部 store.setStateconfig 内部的 set),在更新完成后决定是否把这次变更记录到历史。
  • 历史以两个栈维护:pastStates(用于 undo)与 futureStates(用于 redo),并提供 undo/redo/clear/pause/resume 等 API

index.ts

import { createStore } from 'zustand';
import { temporalStateCreator } from './temporal';
import type {
  StateCreator,
  StoreMutatorIdentifier,
  Mutate,
  StoreApi,
} from 'zustand';
import type {
  TemporalState,
  _TemporalState,
  Write,
  ZundoOptions,
} from './types';

type Zundo = <
  TState,
  Mps extends [StoreMutatorIdentifier, unknown][] = [],
  Mcs extends [StoreMutatorIdentifier, unknown][] = [],
  UState = TState,
>(
  config: StateCreator<TState, [...Mps, ['temporal', unknown]], Mcs>,
  options?: ZundoOptions<TState, UState>,
) => StateCreator<
  TState,
  Mps,
  [['temporal', StoreApi<TemporalState<UState>>], ...Mcs]
>;

declare module 'zustand/vanilla' {
  interface StoreMutators<S, A> {
    temporal: Write<S, { temporal: A }>;
  }
}

export const temporal = (<TState>(
  config: StateCreator<TState, [], []>,
  options?: ZundoOptions<TState>,
): StateCreator<TState, [], []> => {
  // 增强用户传入的 config:通过注入 temporal 子 store,为外层 store 增加“时间旅行”能力
  const configWithTemporal = (
    set: StoreApi<TState>['setState'],
    get: StoreApi<TState>['getState'],
    store: Mutate<
      StoreApi<TState>,
      [['temporal', StoreApi<TemporalState<TState>>]]
    >,
  ) => {
    // 创建一个独立的 temporal 子 store,用来维护历史/未来队列;支持 wrapTemporal 进行自定义包装
    store.temporal = createStore(
      options?.wrapTemporal?.(temporalStateCreator(set, get, options)) ||
        temporalStateCreator(set, get, options),
    );

    // 取出内部的 _handleSet;若用户提供 handleSet,则对其进行柯里化以在外层 set 完成后统一写入时间线
    const curriedHandleSet =
      options?.handleSet?.(
        (store.temporal.getState() as _TemporalState<TState>)
          ._handleSet as StoreApi<TState>['setState'],
      ) || (store.temporal.getState() as _TemporalState<TState>)._handleSet;

    // 在每次状态变更后调用:基于 partialize 获取最小化状态,使用 diff/equality 判断是否需要记录
    const temporalHandleSet = (pastState: TState) => {
      if (!store.temporal.getState().isTracking) return;

      const currentState = options?.partialize?.(get()) || get();
      const deltaState = options?.diff?.(pastState, currentState);
      if (
        // 当 diff 返回 null 或 equality 判断两者相等时,认为无实际变更,跳过记录
        !(
          (
            deltaState === null ||
            options?.equality?.(pastState, currentState)
          )
        )
      ) {
        // 将一次变更写入时间线:pastState -> currentState;deltaState 用于缩减历史体积
        curriedHandleSet(
          pastState,
          undefined as unknown as Parameters<typeof set>[1],
          currentState,
          deltaState,
        );
      }
    };

    const setState = store.setState;
    // 代理原始 setState:先读取变更前的 pastState,再执行真实 setState,最后记录到时间线
    store.setState = (...args) => {
      // 先 get 再 set:保证拿到变更前的最新快照(callback 可能返回部分状态)
      const pastState = options?.partialize?.(get()) || get();
      setState(...(args as Parameters<typeof setState>));
      temporalHandleSet(pastState);
    };

    return config(
      // 同样代理用户传入的 set:确保每次变更都进入时间线(先取快照,再 set,最后记录)
      (...args) => {
        // 先 get 再 set:保证拿到变更前的最新快照
        const pastState = options?.partialize?.(get()) || get();
        set(...(args as Parameters<typeof set>));
        temporalHandleSet(pastState);
      },
      get,
      store,
    );
  };
  // 返回增强后的配置函数,使该 store 具备 temporal 能力(类型层面暴露 mutator)
  return configWithTemporal as StateCreator<TState, [], []>;
}) as unknown as Zundo;

export type { ZundoOptions, Zundo, TemporalState };

temporal.ts

import type { StateCreator, StoreApi } from 'zustand';
import type { _TemporalState, ZundoOptions } from './types';

export const temporalStateCreator = <TState>(
  userSet: StoreApi<TState>['setState'],
  userGet: StoreApi<TState>['getState'],
  options?: ZundoOptions<TState>,
) => {
  // 构造 temporal 子 store 的状态机:维护 pastStates/futureStates,提供 undo/redo 等能力
  const stateCreator: StateCreator<_TemporalState<TState>, [], []> = (
    set,
    get,
  ) => {
    return {
      // 初始化历史与未来队列(可通过 options 预设)
      pastStates: options?.pastStates || [],
      futureStates: options?.futureStates || [],
      // 撤销 steps 步:从过去队列尾部取出状态并应用到用户 store
      undo: (steps = 1) => {
        if (get().pastStates.length) {
          // 必须先拿到变更前的快照:userGet 在 userSet 之前调用
          const currentState = options?.partialize?.(userGet()) || userGet();

          const statesToApply = get().pastStates.splice(-steps, steps);

          // 应用最新的一条历史状态,并将当前状态与剩余历史(反转以维持时间顺序)推入未来队列
          const nextState = statesToApply.shift()!;
          userSet(nextState);
          set({
            pastStates: get().pastStates,
            futureStates: get().futureStates.concat(
              // 若提供 diff,则使用 current->next 的 delta 作为存储单位;否则存完整 currentState
              options?.diff?.(currentState, nextState) || currentState,
              statesToApply.reverse(),
            ),
          });
        }
      },
      // 重做 steps 步:从未来队列尾部取出状态并应用到用户 store
      redo: (steps = 1) => {
        if (get().futureStates.length) {
          // 必须先拿到变更前的快照:userGet 在 userSet 之前调用
          const currentState = options?.partialize?.(userGet()) || userGet();

          const statesToApply = get().futureStates.splice(-steps, steps);

          // 应用最新的一条未来状态,并将当前状态与剩余未来(反转以维持时间顺序)推入过去队列
          const nextState = statesToApply.shift()!;
          userSet(nextState);
          set({
            pastStates: get().pastStates.concat(
              // 若提供 diff,则使用 current->next 的 delta;否则存完整 currentState
              options?.diff?.(currentState, nextState) || currentState,
              statesToApply.reverse(),
            ),
            futureStates: get().futureStates,
          });
        }
      },
      // 清空时间线
      clear: () => set({ pastStates: [], futureStates: [] }),
      // 记录开关:暂停/恢复时间线写入
      isTracking: true,
      pause: () => set({ isTracking: false }),
      resume: () => set({ isTracking: true }),
      // 动态设置保存回调(如持久化)
      setOnSave: (_onSave) => set({ _onSave }),
      // 内部属性与写入逻辑
      _onSave: options?.onSave,
      _handleSet: (pastState, replace, currentState, deltaState) => {
        // 容量限制:若超过 limit,则丢弃最早的过去状态
        if (options?.limit && get().pastStates.length >= options?.limit) {
          get().pastStates.shift();
        }

        // 触发保存回调(可用于外部持久化或统计)
        get()._onSave?.(pastState, currentState);
        set({
          // 将 delta(若存在)或完整 pastState 记录到过去队列,同时清空未来队列
          pastStates: get().pastStates.concat(deltaState || pastState),
          futureStates: [],
        });
      },
    };
  };

  // 对外以普通 StateCreator 形式暴露,隐藏内部 temporal 扩展的具体细节
  return stateCreator as StateCreator<_TemporalState<TState>, [], []>;
};

历史记录是快照还是操作?

  • 默认:存的是“快照”,即在没有提供 diff 时,记录项为“过去的跟踪状态子集
  • 可选:如果提供了 diff,则存的是“最小补丁”
❌