阅读视图

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

Canvas签名功能常见的几种问题

. 如何实现基础签名功能?


<!DOCTYPE html>
<canvas id="signature" width="500" height="300"></canvas>
<script>
  const canvas = document.getElementById('signature');
  const ctx = canvas.getContext('2d');
  let isDrawing = false;
  
  canvas.addEventListener('mousedown', startDrawing);
  canvas.addEventListener('mousemove', draw);
  canvas.addEventListener('mouseup', stopDrawing);
  canvas.addEventListener('mouseout', stopDrawing);

  function startDrawing(e) {
    isDrawing = true;
    ctx.beginPath();
    ctx.moveTo(e.offsetX, e.offsetY);
  }

  function draw(e) {
    if (!isDrawing) return;
    ctx.lineTo(e.offsetX, e.offsetY);
    ctx.stroke();
  }

  function stopDrawing() {
    isDrawing = false;
  }
</script>

1. 如何检测签名是否为空?

function isCanvasBlank(canvas) {
  // 获取画布像素数据
  const context = canvas.getContext('2d');
  const pixelBuffer = new Uint32Array(
    context.getImageData(0, 0, canvas.width, canvas.height).data.buffer
  );
  
  // 检查是否有非透明像素
  return !pixelBuffer.some(color => color !== 0);
}

2. 如何处理不同设备DPI问题?

function setupHighDPICanvas(canvas) {
  // 获取设备像素比
  const dpr = window.devicePixelRatio || 1;
  
  // 获取CSS显示尺寸
  const rect = canvas.getBoundingClientRect();
  
  // 设置实际尺寸为显示尺寸乘以像素比
  canvas.width = rect.width * dpr;
  canvas.height = rect.height * dpr;
  
  // 缩放上下文以匹配CSS尺寸
  const ctx = canvas.getContext('2d');
  ctx.scale(dpr, dpr);
  
  // 设置CSS尺寸保持不变
  canvas.style.width = `${rect.width}px`;
  canvas.style.height = `${rect.height}px`;
}

3. 如何实现撤销/重做功能?

class SignatureHistory {
  constructor(canvas) {
    this.canvas = canvas;
    this.ctx = canvas.getContext('2d');
    this.history = [];
    this.currentStep = -1;
  }
  
  saveState() {
    // 截取当前画布状态
    const imageData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
    
    // 如果当前不是最新状态,截断历史
    if (this.currentStep < this.history.length - 1) {
      this.history = this.history.slice(0, this.currentStep + 1);
    }
    
    this.history.push(imageData);
    this.currentStep++;
  }
  
  undo() {
    if (this.currentStep > 0) {
      this.currentStep--;
      this.ctx.putImageData(this.history[this.currentStep], 0, 0);
    }
  }
  
  redo() {
    if (this.currentStep < this.history.length - 1) {
      this.currentStep++;
      this.ctx.putImageData(this.history[this.currentStep], 0, 0);
    }
  }
}

4. 如何添加签名压力感应效果?

// 监听指针事件(支持压力感应设备)
canvas.addEventListener('pointerdown', startDrawing);
canvas.addEventListener('pointermove', drawWithPressure);
canvas.addEventListener('pointerup', stopDrawing);

function drawWithPressure(e) {
  if (!isDrawing) return;
  
  // 获取压力值(0-1),默认0.5用于鼠标
  const pressure = e.pressure || 0.5;
  
  // 根据压力调整线条宽度
  ctx.lineWidth = pressure * 10 + 2; // 2-12px范围
  
  ctx.lineTo(e.offsetX, e.offsetY);
  ctx.stroke();
  ctx.beginPath();
  ctx.moveTo(e.offsetX, e.offsetY);
}

5. 如何防止签名图片被篡改?

function generateSignatureHash(canvas) {
  // 获取画布数据
  const imageData = canvas.toDataURL('image/png');
  
  // 使用SHA-256生成哈希
  return crypto.subtle.digest('SHA-256', new TextEncoder().encode(imageData))
    .then(hash => {
      // 转换为十六进制字符串
      const hashArray = Array.from(new Uint8Array(hash));
      return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
    });
}

6. 如何加密存储签名数据?

async function encryptSignatureData(canvas) {
  // 获取画布数据
  const imageData = canvas.toDataURL('image/png');
  
  // 生成加密密钥
  const key = await crypto.subtle.generateKey(
    { name: 'AES-GCM', length: 256 },
    true,
    ['encrypt', 'decrypt']
  );
  
  // 加密数据
  const iv = crypto.getRandomValues(new Uint8Array(12));
  const encrypted = await crypto.subtle.encrypt(
    { name: 'AES-GCM', iv },
    key,
    new TextEncoder().encode(imageData)
  );
  
  return {
    key,
    iv,
    encryptedData: Array.from(new Uint8Array(encrypted))
  };
}

7. 如何实现多人协同签名?

class CollaborativeSignature {
  constructor(canvas, socket) {
    this.canvas = canvas;
    this.ctx = canvas.getContext('2d');
    this.socket = socket;
    
    // 本地绘制事件
    canvas.addEventListener('mousedown', this.handleLocalDrawStart.bind(this));
    canvas.addEventListener('mousemove', this.handleLocalDrawing.bind(this));
    canvas.addEventListener('mouseup', this.handleLocalDrawEnd.bind(this));
    
    // 远程绘制事件
    socket.on('remote-draw-start', this.handleRemoteDrawStart.bind(this));
    socket.on('remote-drawing', this.handleRemoteDrawing.bind(this));
    socket.on('remote-d

# Vue 中 provide/inject 与 props/emit 的对比与选择

一、核心设计理念差异

1. 数据流向的明确性

props/emit‌ 遵循严格的单向数据流:

// 父组件
<child-component :message="parentMsg" @update="handleUpdate" />

// 子组件
export default {
  props: ['message'],
  methods: {
    sendToParent() {
      this.$emit('update', newValue) // 明确的数据流向
    }
  }
}

provide/inject‌ 则是隐式的跨层级通信:

// 祖先组件
provide('sharedData', reactive({ value: null }))

// 任意后代组件
const data = inject('sharedData')
data.value = 123 // 来源不直观

二、必须使用 props/emit 的场景

1. 可复用组件开发

组件库中的按钮组件‌:

// 使用props定义明确接口
export default {
  props: {
    type: {
      type: String,
      default: 'default',
      validator: val => ['default', 'primary', 'danger'].includes(val)
    },
    disabled: Boolean
  },
  emits: ['click'], // 显式声明事件
  template: `
    <button 
      :class="['btn', `btn-${type}`]"
      :disabled="disabled"
      @click="$emit('click', $event)"
    >
      <slot></slot>
    </button>
  `
}

2. 父子组件明确契约

表单验证场景‌:

// 父组件
<validated-input
  :rules="[v => !!v || '必填项']"
  @valid="isFormValid = $event"
/>

// 子组件
export default {
  props: ['rules'],
  emits: ['valid'],
  watch: {
    inputValue() {
      const isValid = this.rules.every(rule => rule(this.inputValue))
      this.$emit('valid', isValid) // 明确的状态反馈
    }
  }
}

三、provide/inject 的适用边界

1. 适合使用 provide/inject 的场景

跨多层组件共享配置‌:

// 主题提供者组件
provide('theme', {
  colors: {
    primary: '#409EFF',
    danger: '#F56C6C'
  },
  darkMode: false
})

// 深层嵌套的按钮组件
const theme = inject('theme')
const buttonColor = computed(() => 
  theme.darkMode ? theme.colors.primary : '#333'
)

2. 不适合使用 provide/inject 的情况

列表项与父组件通信‌:

// 错误示范:使用inject修改父级状态
inject('parentMethods').updateItem(item) // 破坏组件独立性

// 正确做法:通过props/emit
props: ['item'],
emits: ['update'],
methods: {
  handleUpdate() {
    this.$emit('update', newItem) // 保持接口明确
  }
}

四、关键对比维度

维度 props/emit provide/inject
组件耦合度 低(明确接口) 高(隐式依赖)
可维护性 容易追踪数据流 调试困难
类型安全 支持完整类型定义 JavaScript中难以类型检查
适用层级 父子/直接关联组件 任意层级组件
测试便利性 可单独测试输入输出 需要构建完整上下文
代码可读性 接口清晰可见 需要查找provide源头

五、实际项目中的混合使用

1. 组合式API最佳实践

// 组件定义
export default {
  props: {
    // 必须的输入
    modelValue: { type: String }
  },
  emits: ['update:modelValue'],
  setup(props, { emit }) {
    // 注入应用级配置
    const appConfig = inject('appConfig')
    
    const handleInput = (e) => {
      // 本地事件处理
      emit('update:modelValue', e.target.value)
      
      // 同时使用注入的方法
      appConfig.trackInput?.(e.target.value)
    }
    
    return { handleInput }
  }
}

2. 设计模式选择指南

graph TD
    A[组件通信需求] --> B{通信方向}
    B -->|父→子| C[props]
    B -->|子→父| D[emit]
    B -->|兄弟组件| E[状态提升/全局状态]
    A --> F{层级深度}
    F -->|1-2层| G[优先props/emit]
    F -->|3+层| H[考虑provide]
    A --> I{复用性要求}
    I -->|高复用组件| J[必须用props/emit]
    I -->|内部实现细节| K[可用provide]

六、典型误用案例分析

1. 滥用 provide 导致的状态混乱

// 问题代码:多个组件通过inject修改同一状态
provide('globalState', reactive({ count: 0 }))

// 组件A
inject('globalState').count++

// 组件B
inject('globalState').count *= 2
// 无法追踪修改来源,调试困难

2. 应该使用 props 的场景

// 错误示范:用inject代替props
provide('userAvatar', avatarUrl)

// 正确做法:头像组件应该通过props接收数据
export default {
  props: {
    avatarUrl: String // 明确接口
  }
}

七、工程化考量

1. 项目可维护性影响

  • props/emit‌ 使组件成为"黑盒",通过接口文档即可理解功能
  • provide/inject‌ 需要查看组件实现才能理解依赖关系

2. 团队协作规范

// 良好的组件接口设计
export default {
  props: {
    // 带验证的props
    size: {
      type: String,
      default: 'medium',
      validator: s => ['small', 'medium', 'large'].includes(s)
    }
  },
  emits: {
    // 带验证的emit
    'size-change': payload => typeof payload === 'string'
  }
}

总结来说,props/emit 提供了组件间明确、可预测的通信方式,是构建可维护、可复用组件的基础;而 provide/inject 是特定场景下的补充方案,适用于真正需要穿透多层级的上下文共享场景。


Vue 中 provide/inject 与传统状态管理的深度对比

一、provide/inject 基础原理

1. 基本用法

// 祖先组件提供数据
export default {
  provide() {
    return {
      theme: 'dark',
      toggleTheme: this.toggleTheme
    }
  },
  methods: {
    toggleTheme() {
      this.theme = this.theme === 'dark' ? 'light' : 'dark'
    }
  }
}

// 后代组件注入使用
export default {
  inject: ['theme', 'toggleTheme'],
  template: `
    <button @click="toggleTheme">
      当前主题: {{ theme }}
    </button>
  `
}

2. 响应式数据传递

// 使用 Vue 3 的 reactive/ref
import { ref, provide } from 'vue'

export default {
  setup() {
    const count = ref(0)
    provide('count', count)
    
    return { count }
  }
}

// 后代组件
export default {
  setup() {
    const count = inject('count')
    return { count }
  }
}

二、provide/inject 的优势

1. 组件树穿透能力

场景‌:多层嵌套组件共享配置

// 根组件
provide('appConfig', {
  apiBaseUrl: 'https://api.example.com',
  features: {
    analytics: true,
    notifications: false
  }
})

// 第5层子组件直接使用
const config = inject('appConfig')
console.log(config.apiBaseUrl) // 直接访问

2. 减少 props 传递

传统方式‌:

// 每层组件都需要传递props
<Parent :config="config">
  <Child :config="config">
    <GrandChild :config="config" />
  </Child>
</Parent>

provide/inject 方式‌:

// 根组件
provide('config', config)

// 任意层级子组件
const config = inject('config')

3. 动态上下文共享

场景‌:表单组件与表单项通信

// Form 组件
provide('formContext', {
  registerField: (field) => { /* 注册字段 */ },
  validate: () => { /* 验证表单 */ }
})

// FormItem 组件
const { registerField } = inject('formContext')
onMounted(() => registerField(this))

三、provide/inject 的劣势

1. 调试困难

// 当多个祖先提供同名key时
const data = inject('settings') // 无法直观确认数据来源

// 解决方案:使用Symbol作为key
const SettingsKey = Symbol()
provide(SettingsKey, { theme: 'dark' })
const settings = inject(SettingsKey)

2. 缺乏状态管理

// 简单的计数器示例
provide('counter', {
  count: 0,
  increment() { this.count++ }
})

// 问题:
// 1. 状态变更无法追踪
// 2. 多个组件修改时可能产生冲突

3. 类型安全缺失(JavaScript中)

// 无法像TypeScript那样进行类型检查
const user = inject('user') // 不知道user的结构

四、与传统状态管理(Vuex)对比

1. Vuex 基本示例

// store.js
export default new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment(state) {
      state.count++
    }
  },
  actions: {
    asyncIncrement({ commit }) {
      setTimeout(() => commit('increment'), 1000)
    }
  }
})

// 组件中使用
export default {
  computed: {
    count() {
      return this.$store.state.count
    }
  },
  methods: {
    increment() {
      this.$store.commit('increment')
    }
  }
}

2. 对比表格

特性 provide/inject Vuex
作用范围 组件树局部 全局
调试工具 不可见 完整的时间旅行调试
响应式 自动响应式 自动响应式
代码组织 分散在各组件 集中式管理
类型安全 需要额外处理 需要类型定义
服务端渲染 天然支持 需要额外配置
性能 按需注入,内存友好 全局存储,初始加载稍慢
适用场景 组件库/局部状态共享 大型应用全局状态管理

五、实际场景选择指南

1. 适合 provide/inject 的场景

场景1:UI组件库开发

// 下拉菜单组件
provide('dropdown', {
  registerItem: (item) => { /* 注册菜单项 */ },
  close: () => { /* 关闭菜单 */ }
})

// 菜单项组件
const { registerItem, close } = inject('dropdown')
onMounted(() => registerItem(this))

场景2:主题切换

// 主题提供者
provide('theme', {
  current: 'light',
  colors: {
    light: { primary: '#fff' },
    dark: { primary: '#000' }
  }
})

// 任意子组件
const { current, colors } = inject('theme')
const bgColor = computed(() => colors[current].primary)

2. 适合 Vuex/Pinia 的场景

场景1:用户全局状态

// store/user.js (Pinia示例)
export const useUserStore = defineStore('user', {
  state: () => ({
    name: '',
    token: ''
  }),
  actions: {
    async login(credentials) {
      const res = await api.login(credentials)
      this.name = res.name
      this.token = res.token
    }
  }
})

// 多个组件共享同一状态
const userStore = useUserStore()
userStore.login({...})

场景2:购物车管理

// store/cart.js (Vuex示例)
{
  state: {
    items: []
  },
  mutations: {
    ADD_ITEM(state, item) {
      state.items.push(item)
    }
  },
  getters: {
    totalPrice: (state) => {
      return state.items.reduce((sum, item) => sum + item.price, 0)
    }
  }
}

// 组件中使用
this.$store.commit('ADD_ITEM', product)
this.$store.getters.totalPrice

六、混合使用模式

1. 全局状态 + 局部增强

// 使用Pinia作为基础
const userStore = useUserStore()

// 在特定组件树中增强功能
provide('enhancedUser', {
  ...userStore,
  // 添加局部方法
  sendMessage() {
    console.log(`Message to ${userStore.name}`)
  }
})

2. 性能优化技巧

javascriptCopy Code
// 避免在provide中直接传递大对象
provide('heavyData', () => fetchHeavyData())

// 组件中按需获取
const getHeavyData = inject('heavyData')
const data = computed(() => getHeavyData())

七、决策流程图

graph TD
    A[需要共享状态?] -->|是| B{状态使用范围}
    B -->|全局多组件| C[Vuex/Pinia]
    B -->|特定组件树| D{状态复杂度}
    D -->|简单配置| E[provide/inject]
    D -->|复杂业务逻辑| C
    A -->|否| F[使用组件本地状态]

总结‌:

  • provide/inject 适合组件库开发和局部状态共享
  • Vuex/Pinia 适合大型应用全局状态管理
  • 在JavaScript项目中,注意通过命名规范和Symbol来避免注入冲突
  • 对于中型项目,可以考虑混合使用两种方案

前端主题色小案例

以下是一个基于Vue3 + Sass的完整主题色解决方案,包含动态切换、状态管理、样式组织等核心模块: 一、项目结构设计 styles/_variables.scss 该文件定义可扩展的色板系统,支持
❌