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 已有5条回复
  1. #974 labike 2020-10-24 11:08 回复
  2. 发现第一次输入之后,鼠标移开的话,文字会消失,第二次移入又出现
    #988 Nelson 2020-12-10 18:18 回复
  3. 有代码吗?给个代码看下
    #989 回复给#988 否子戈 2020-12-13 20:55 回复
  4. 已经解决了,是我自己的问题
    #990 回复给#989 Nelson 2020-12-15 12:39 回复
  5. !!!太感谢了,真的好用,困扰了我一天的问题,您的讲解真的是太清楚明朗了!
    #1172 o797 2022-03-04 18:15 回复
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 已有1条回复
  1. 牛逼!
    #1297 2023-11-12 02:31 回复
252020.7

DDD设计理念思维导图

13:28:46 已有0条回复
192020.7

四种应该知道的软件架构

152020.7

如何获取浏览器指纹

082020.7

和leader阿义的对话中,他指出,现在前端面试或许已经不适用原来的前端能力模型了,传统要求性能、对基础知识的全面掌握,可能已经不适用现在的招聘需求,前端分工明显,企业招聘更多希望是招到合适的立马可以上手的人,但一个人精力有限,不可能所有知识和能力都掌握。所以,细化的能力模型越来越突出,比如我们团队而言,在性能这个点上的要求并没有那么高,但是在对业务的把控、代码的稳定性上要求很高,因为我们对接的是toB的产品,不同产品对代码、系统的要求重点不同,所以不能一概而论的对应聘者进行要求。

09:41:54 已有1条回复
  1. 吾辈更倾向于招优秀(喜欢学习和自我驱动)的人,而不是合适但不愿变化的人(入职即巅峰)╮( •́ω•̀ )╭
    #940 rxliuli 2020-07-09 07:39 回复
292020.6

Creating custom JavaScript syntax with Babel

212020.6

Does DDD Belong on the Frontend? - Domain-Driven Design w/ TypeScript

172020.6

NPM 依赖包版本范围控制

基本概念就不多说了,主要是想在项目中,使用“范围”控制的优势,写了一个依赖版本号之后,未来依赖包升级可以不需要手动升级该版本号,可自动安装最新兼容版本。

常规兼容

常规情况下,需要掌握 ^, ~, x, - 这几个版本控制符即可。例如 ^1.0.0 相当于 1.x,~1.1.0 相当于 1.1.x。

实验阶段的包

版本号以 0 开头的包都是实验阶段的。例如 0.0.1, 0.1.0 这种包都是处于实验阶段,还为发布正式版本,因此,在正式环境使用时要小心。基于这种情况,我建议直接写死版本号,而不要采用 ^ 或 ~,因为你怎么知道新加的实验功能是否符合你的预期。

当然,纯理论探讨,实验阶段的包也可以 ^ 或 ~,但是相对而言,全部降一位。例如 ^0.1.0 可以兼容 0.1.1 版本,但不兼容 0.2.0 版本。如果 ~0.0.1,相当于没写 ~。

Prerelease

和实验阶段包又不同,它处于预发布阶段。比如在发布 3.0.0 之前,可能需要公测一段时间,各种修 bug。但理论上,prerelease 相当于正式版的实验阶段。

对于 prerelease 版本而言,前面的常规方案和实验阶段常规方案,都不包含 prerelease 版本,也就是说 ^3.0.0 并不包含 3.1.0-alpha.1。但是反过来,3.1.0-alpha.1 属于 3.1.0 的一部分,用一个比喻来类比:A-, A, A+ 都属于 A 等。但是 ~A, ^A 都只包含 A, A+ 而不包含 A-,类比数学上的概念,就是复数中虚数部分。

那么怎么控制 prerelease 的范围呢?

^3.0.0-alpha 将包含 3.0.0-alpha.0, 3.0.0-beta.0, 3.0.0-rc, 3.0.0, 3.1.0...,但不包含 3.1.0-alpha.x。也就是说,^ 或 ~ 可以包含所指示的版本的 prerelease 版本,以及兼容该版本的高版本(不含高版本的 prerelease 版本)。

升级 prerelease 可以使用 npm version prerelease 进行升级。

两个 prerelease 版本号谁的版本更高呢?prerelease 末尾版本号部分纯粹以字母顺序进行排序,因此 rc > beta > alpha,而且,由于纯粹按字母排序,所以没有 alpha.x 这种使用 x 的表示方法。因此,如果要控制为只包含 prerelease 的版本,没有直接的办法,只能使用 >=3.0.0-alpha <3.0.0-beta 这种表示方法。

12:07:28 已有0条回复
112020.6

纯CSS实现Table固定表头和首列

Table固定表头和首列这种需求应该比较常见。以往的做法,需要写一大堆脚本,而现在,可以使用position:sticky轻松实现这个效果。

.table-container {
width: 100%;
height: 100%;
overflow: auto;
}

/* 首列固定 */
.table-container thead tr > th:first-child,
.table-container tbody tr > td:first-child {
position: sticky;
left: 0;
z-index: 1;
}

/* 表头固定 */
.table-container thead tr > th {
position: sticky;
top: 0;
z-index: 2;
}

/* 表头首列强制最顶层 */
.table-container thead tr > th:first-child {
z-index: 3;
}

HTML结构上,必须将 <table> 放在 <div class="table-container"> 子节点,且内部不要有其他 position 设置。

14:48:16 已有2条回复
  1. 但是在表格上面还有其他内容的时候纯css就比较难,头部的内容不能左右滚动,
    div - 宽带 屏幕100% 可以上下滚动,不能左右滚动

    表格 - 上下左右滚动, 首行吸顶,首列固定

    没找到不需要js的解决方案
    #1104 Blaze 2021-11-12 00:59 回复
  2. 肯定的,css的能力是描述性质,很难像编程一样灵活
    #1107 回复给#1104 否子戈 2021-11-15 22:28 回复