222020.8

《数字水印原理与技术》

Photo Copy 是一个用于给照片打水印的小软件,基于盲水印的实现原理,利用 electron 对 nodejs 的支持,利用 opencv 实现了盲水印。

上个月我发布了第一个预览版,没什么人用,但我自己还是很想把它做好,所以我在原来的基础上,重新梳理了一些点。其中最重要的一个点,是对原来垂直的图片进行了处理,之前垂直的图片放进去之后,直接横着打水印,导致水印不全,于是我想了一个办法,把图片竖过来,打完水印再翻回来,这样水印就可以完整的显示出来了。另外,之前没有支持拖拽上传,现在也支持了。你可以在这里下载。

19:40:24 已有0条回复
132020.8

解决react input受控组件无法输入中文

刚遇到 react input 受控组件无法输入中文的情况:

<input value={value} onChange={onChange} />

在网上搜索了很多,知道了问题原因,但是大部分文献都实际上没有解决问题。网上大部分文献,只解决了 onChange 的问题,但是没有解决 value 和 onChange 一起用的问题。value 属性导致 input 成为一个完完全全的受控组件。value 的值决定当前 input 的展示内容。因此,网上的文献只提供了一些参考,并不能解决问题本质。

因 value 属性导致无法输入中文的原因在于,react 会用 onChange 这个接口响应任意输入,而如果在此时修改 value 值,就会导致 DOM 的重绘(react 通过实时处理 input 的 DOM 节点的 value 属性值来保证重绘后使用正确的 value,你可以通过开发者工具查看这个 input 节点在输入时的变化),直接导致中文输入法框消失,输入框内变成刚才输入的英文字符(甚至带空格)。onChange 接管 input 的一系列属性,它不是单纯的 change 或 input 事件。当然,在 type=text 上,它主要功能是接管 input,但是 onInput 实际上还是有效。

我在 jqvm 中实际上也遇到这个问题。当时发现通过响应式方法替换真实 DOM 之后,原有的输入态会消失(没有 focus),所以后来解决办法是,保存原始真实 DOM,每次更新后用原来的 DOM 把新绘入的 DOM 给替换掉,再做一些输入态的处理(例如光标位置)。

显然,react 的处理方式不是这样,react 也保存了真实 DOM,但是每次更新 value 之后,会对 DOM 节点的 value 进行复杂加工。不过理论上,react 面临同样的问题,所以它应该也要解决这个输入态问题,这还会牵扯到 focus 和 blur,下面会提到。总而言之,要解决无法输入中文的问题,还是要从真实 DOM 入手解决。

import React, { useRef } from 'react'

export function Input(props) {
  const el = useRef(null)
  const { onChange, onFocus, onBlur, value, defaultValue, ...attrs } = props
  const _value = ('value' in props) ? value : ('defaultValue' in props) ? defaultValue : null
  const handleChange = (e) => {
    if (onChange) {
      onChange(e)
    }
  }
  const forceSetValue = () => {
    if ('value' in props && el.current) {
      const input = el.current
      input.value = value
      input.setAttribute('value', value)
    }
  }

  let inputing = false

  return (
    <input
      {...attrs}
      defaultValue={_value}
      ref={(input) => {
        if (!input) {
          return
        }

        el.current = input
        forceSetValue()
      }}

      // react 在focus/blur时会重新设值,如果没有下面的操作,会导致focus/blur之后,变空
      // TODO 由于是异步操作,会导致文字闪动,光标定位到最末尾
      onFocus={(e) => {
        setTimeout(forceSetValue, 10)
onFocus && onFocus(e) }} onBlur={(e) => { setTimeout(forceSetValue, 150)
onBlur && onBlur(e) }} onCompositionStart={() => { inputing = true }} onCompositionEnd={(e) => { inputing = false handleChange(e) }} onChange={(e) => { if (!inputing) { handleChange(e) } }} /> ) } export default Input

首先第一步,就是借鉴网上资料提供的方法,干掉 value,使用 defaultValue。完成这一步之后,就可以实现正常输入中文了(从这里可以看出无法输入中文的罪魁祸首还是受控机制)。接下来的问题就是

  • 既要能够通过 onChange 把修改后的值传出去,
  • 同时又要能保证 value 属性和真实 DOM 的 value 是同步的。

这里有个问题,input 组件的 defaultValue 属性会带来新的问题,如果由于切换视图当前组件被销毁了,再切换回来,此时由于 defaultValue 属性的逻辑,input 的显示内容不会变成新传入的 defaultValue 的值,而是仍然采用之前的值。假如你一进入界面的时候,defaultValue 是空的(defaultValue={value} value 为空),那么当你输入了一波,修改了 value,然后切走了,但是你想切回来重新继续输入(defaultValue={value} 此时 value 不为空),你会发现 input 内容空空如也。这是 defaultValue 的弊端,它让 input 是非受控的。这也是为什么,我们无论用 value 还是 defaultValue 都无法满足我们正常输入中文的问题。

通过 onChange 把修改后的值传出去这个好办。我在代码里面加了 compositions 控制,但是实际上不需要也无所谓,大不了就是 onChange 传出去的值是不怎么好看的字符串(带空格的拼音),加了 compositions 控制,保证每次 onChange 出去的都是完整的中文字符串,在拼音输入期间并没有触发 onChange,外部也不会接收到。所以,本质上,无法输入中文的问题,和 compositions 无关,这个结论可能和网上大部分资料的说法都相反。

保证当前这个 Input 组件的 value 和真实 DOM 的 value 的同步,则是我这个解决办法的核心,依托真实 DOM,直接将真实 DOM 节点的 value 和 value attribute 都修改为和外部传入的 value 值相等,这样这个组件虽然还是 react 组件,但是已经在 DOM 层面被我魔改了。通过这个魔改,解决了上面所说的 defaultValue 无法控制 input 的问题。现在,在形式上,Input 组件接收 value 和 onChange 两个 prop,它是一个完全支持中文输入的受控组件了。

不过 react 又干了些多余的事,在每次 focus 和 blur 时,都会去检查组件受控情况和 value 值,导致非受控组件的内部状态每次都是初始值(当第一次进来 defaultValue 为空时,那么这个 input 就一直都是空)。所以,我又加了 onFocus 和 onBlur 两个属性,在里面通过一个延时操作,在 react 完成自己的检查之后,我再去修改真实 DOM,解决这个问题。

简单总结,react 受控组件是一个大坑。

13:09:54 已有0条回复
072020.8

基于jQuery的500行小型响应式框架jQvm

因为做一个用于生成水印的小型工具,使用到electron作为壳,在选ui框架的时候,当然是首先想到vue,但是经过一会儿使用后,发现vue机制在electron下面无法直接使用脚本引入方式,有些遗憾,于是打算直接使用jquery。但是写了一会儿之后,又觉得别扭,写react和vue太久,通过修改状态来触发界面重绘,这种方式实在太顺手了,现在去写jquery的代码,实在有些回不去的感觉。

于是乎,我自己写了一个基于jquery的响应式插件,也可以把它当作是一个小框架。

github.com/tangshuang/jqvm

具体使用方法如下:

<script src="https://unpkg.com/jquery/dist/jquery.min.js"></script>
<script src="https://unpkg.com/jqvm/dist/jqvm.min.js"></script>

<template id="app">
  <span>{{name}}</span>
  <span>{{age}}</span>
  <button>grow</button>
</template>

<script>
$('#app')
  .vm({
    name: 'tomy',
    age: 12,
  })
  .on('click', 'button', state => {
    state.age ++
  })
  .mount()
</script> 

以上就是最简单的用法。模板定义和vue差不多,但是有个大的区别,vue直接在模板里面绑定事件,但是在jqvm中通过传统的jquery的on绑定事件。

它有一些内置的directive(指令),最常用的应该是jq-if了。

<template>
  <div jq-if="isTouched">xxx</div>
</template> 

还可以在mount 之前注册一些component和directive。

const { component, directive } = $.vm

component('icon', function(el, attrs) {
  const { type } = attrs
  return `<i class="icon icon-${type}"></i>`
})

directive('jq-link', function(el, attrs) {
  const link = attrs['jq-link']
  const to = this.parse(link) // this.parse 是一个内置服务,用来将字符串解析成 state 上的对应值
  el.attr('href', to)
})

注册好之后,在模板里面使用。

<template>
  <icon type="search"></icon>
  <a jq-link="xxx">jump</a>
</template>

再来说说设计理念,我的想法,就是“简单”。使用简单,也别整特别多概念,虽然我前面提到vm, state之类的,一看基本上都能明白咋回事,拿过来就撸,别想那么多。不过还是需要再解释一下on那里怎么绑定事件,第三个参数是一个函数,这个函数接收state,这个state被修改就会重新渲染,这个函数返回一个函数,返回的这个函数就是正常情况下我们用 jquery.fn.on绑定的时候传入的那个函数。

$('#app')
  .vm({ ... })
  .on('click', 'button', state => function(e) {
    const color = $(this).css('color')
    state.color = color
    // ...
  })

这个小框架打包压缩之后,总体体积不到50k,可以说麻雀虽小,五脏俱全。也没有啥特别新潮的东西,在一些需要1分钟内开始界面编程的场景,再适合不过了。

github.com/tangshuang/jqvm

如果你觉得这个项目有点意思,给个star吧。

08:07:17 已有0条回复