点击编辑文字组件实战:React/Vue实现、事件处理与中文输入法避坑
2026/9/8 15:16:07 网站建设 项目流程

在实际前端项目中,点击编辑文字(inline edit / click-to-edit)是一种非常高频出现的交互形态。用户看到一截文本,点击后它变成输入框,编辑完成后按回车或点其他地方,内容又恢复成文本展示,整个过程不离开当前页面。这种交互常见于个人资料页的昵称、仪表盘标题、表格单元格、订单备注、任务名称等地方。很多初学者会把它当作“点击后换成输入框”来处理,但真正落地时会遇到事件冒泡、失焦顺序、中文输入法误提交、空值校验、焦点管理等一系列问题。这篇文章从浏览器原生实现讲起,再封装成 React 和 Vue 的可复用组件,重点解决状态切换、键盘操作、输入校验和可访问性这些在真实项目中一定会踩到的细节。

1. 点击编辑文字的交互原理,远不止“点击后换成输入框”

“点击编辑文字”看上去只是一个交互小技巧,但它背后是一套完整的状态转换流程。如果不先把状态和事件边界理清楚,写出来的组件会在边界场景里四处漏风。

1.1 三个核心状态:查看态、编辑态、保存态

一个健壮的点击编辑组件,至少包含两个状态:查看态和编辑态。如果保存动作涉及异步请求,还会有第三个状态:保存态。

  • 查看态:页面展示一段文本,鼠标悬停时有可点击提示。此时组件内部持有真实数据 value。
  • 编辑态:点击文本后,文本区域被 input 或 textarea 替换,输入框内回填当前 value,并自动聚焦、选中全文字。此时输入的是 draft,而不是直接改 value。
  • 保存态:按回车、失焦或点击保存按钮后,组件把 draft 提交给父组件。如果提交是异步接口,需要用 loading 状态防止重复提交;接口失败时还要把显示内容回滚到旧 value。

状态之间的转换可以用下面这张表概括:

状态触发事件界面表现数据变化
查看态组件初始渲染显示文本或占位符value 负责展示
查看态到编辑态点击文本、键盘 Enter 或空格文本隐藏,输入框出现draft 初始化为 value
编辑态到查看态按 Enter、输入框失焦输入框隐藏,文本更新提交 draft 给 onSave
编辑态到查看态按 Escape输入框隐藏,文本不变draft 直接丢弃

关键设计点是:编辑过程中永远不要直接修改 value,而是维护一份 draft。这样用户按 Escape 时,可以无损取消;调用接口失败时,也可以把 value 回滚回去。

1.2 三种文本编辑载体怎么选

实现点击编辑,最常见的载体是<input><textarea>contenteditable。三者看起来都能编辑文字,但适用场景差异很大。

载体适合场景值获取方式主要风险
input单行短文本,如昵称、标题、标签input.value无法换行,需要控制宽度
textarea多行备注、简介、描述textarea.value需要处理高度自适应和换行
contenteditable富文本、带格式内容innerText / innerHTML值难以统一、易携带标签、有 XSS 风险

对于普通的“点击编辑文字”场景,推荐优先使用 input 和 textarea。它们天然受控,浏览器提供的 value 获取方式稳定,也不会让用户粘贴进来的 HTML 污染页面。

1.3 常见的坑:不要用 contenteditable 做纯文本编辑

contenteditable 最大的问题是“看似简单,实则不可控”。用户编辑后,浏览器可能保留大量内联标签;不同浏览器对换行、段落的处理规则不一致;提取值时要考虑 innerText 和 innerHTML 的取舍;如果直接把用户输入塞回页面,还会产生 HTML 注入风险。

如果只是编辑一行标题或一段纯文本,用 input 或 textarea 就够了,不要为了“少写一个组件”而引入 contenteditable。真正要展示富文本时,应该考虑成熟的富文本编辑器,而不是自己从零维护 contenteditable 的复杂状态。

2. 环境准备:用 Vite + React + TypeScript 承接后面的代码

为了让文章里的代码可以一键跑起来,下面用一个 Vite 项目作为演示环境。如果你更熟悉 Vue,跳过 React 示例、直接看 Vue 章节也没有问题,两套实现的核心逻辑是同一套。

2.1 前置版本要求

不同工具链对 Node 版本要求不同,落地前先确认本机环境。下面的版本建议偏向保守:

工具建议版本说明
Node.js18 或更高Vite 5 及以上版本要求 Node 18+
npm9.x 及以上或使用 pnpm、yarn 替代
React18.x基于函数式组件和 Hooks
TypeScript5.x建议开启,组件 props 更清晰
Vue3.x使用组合式 API 和<script setup>

检查方式是在终端执行:

node -v npm -v

如果本机 Node 版本较低,建议先升级到 LTS 版本再继续,否则npm create vite可能报版本兼容错误。

2.2 创建项目

使用 Vite 官方脚手架创建 React + TypeScript 项目:

npm create vite@latest click-to-edit-demo -- --template react-ts cd click-to-edit-demo npm install npm run dev

如果创建 Vue 版本,把模板参数换成 vue-ts 即可:

npm create vite@latest click-to-edit-vue -- --template vue-ts

启动后浏览器访问终端输出的本地地址,能看到 Vite 默认页面,说明环境就绪。这里要注意,npm create命令在不同 npm 版本下,--后面的参数解析方式可能不同,如果提示模板参数无法识别,换成npm create vite@latest click-to-edit-demo --template react-ts再试。

2.3 项目目录结构

React 版本中,新建组件文件后,目录大致如下:

click-to-edit-demo/ ├── index.html ├── package.json ├── tsconfig.json ├── vite.config.ts └── src/ ├── main.tsx ├── App.tsx └── components/ └── ClickToEditText.tsx

后面文章里,ClickToEditText.tsx就是核心组件,App.tsx负责引用组件并提供演示数据。Vue 版本对应的是src/components/ClickToEditText.vue

3. 先写原生版本,把浏览器事件边界看清楚

在封装框架组件之前,先用原生 JavaScript 写一版最小的“点击编辑文字”。这样做的好处是能直接看到浏览器事件细节,不会被 JSX 和响应式语法干扰。

3.1 页面结构与样式

先用一个 span 展示文本,一个 input 负责编辑。默认隐藏 input,点击 span 后才显示。

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <title>点击编辑文字 - 原生示例</title> <style> .editable-item { display: inline-flex; align-items: center; gap: 8px; font-size: 16px; } .editable-text { cursor: pointer; padding: 4px 8px; border-radius: 4px; } .editable-text:hover { background: #f0f2f5; } .editable-input { padding: 4px 8px; font-size: 16px; border: 1px solid #1890ff; border-radius: 4px; outline: none; width: 200px; } </style> </head> <body> <div class="editable-item"> <span id="displayText" class="editable-text" title="点击编辑">前端开发小张</span> <input id="editInput" class="editable-input" type="text" style="display: none" /> </div> <script src="./index.js"></script> </body> </html>

这里的关键是:span 负责查看态,input 负责编辑态,两者通过display属性切换。title="点击编辑"给鼠标用户一个提示,这是最基本的可用性兜底。

3.2 原生 JavaScript 的事件逻辑

编辑逻辑包括四个核心事件:点击进入编辑、回车提交、Escape 取消、失焦提交。还需要处理中文输入法组合输入。

const displayText = document.getElementById('displayText'); const editInput = document.getElementById('editInput'); let composing = false; function enterEdit() { editInput.value = displayText.textContent.trim(); editInput.style.display = 'inline-block'; displayText.style.display = 'none'; editInput.focus(); editInput.select(); } function commit(save) { if (!save) { editInput.style.display = 'none'; displayText.style.display = ''; return; } const next = editInput.value.trim(); editInput.style.display = 'none'; displayText.style.display = ''; if (next.length === 0) { displayText.textContent = '点击编辑'; return; } displayText.textContent = next; } displayText.addEventListener('click', enterEdit); editInput.addEventListener('blur', () => commit(true)); editInput.addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); if (composing) return; commit(true); } if (e.key === 'Escape') { e.preventDefault(); commit(false); } }); editInput.addEventListener('compositionstart', () => { composing = true; }); editInput.addEventListener('compositionend', () => { composing = false; });

代码里有几个值得注意的点:

  • commit(true)统一处理“回车提交”和“失焦提交”,避免逻辑分散。
  • editInput.select()让用户点击后可以立即覆盖整个旧值,这是点击编辑的标准体验。
  • composing标志位用来判断中文输入法是否正在组合拼音。用户输入“nǐ hǎo”时,按回车是选字而不是确认提交,此时不能触发 commit。这个坑在中文项目里极其常见。

3.3 这版代码能运行,但它有几处工程隐患

原生版本可以完整演示交互原理,但直接搬到真实项目会有几个问题:

  1. 逻辑无法复用。页面里如果有十个可编辑文本,就要复制十份 DOM 查询和事件绑定。
  2. 没有校验。空值、超长内容、纯空格都没有处理策略,保存后可能得到脏数据。
  3. 没有异步保存。真实场景里 onSave 往往要调接口,失败时需要回滚,成功时需要更新父组件数据。
  4. 焦点管理缺失。Escape 取消后,键盘焦点停留在隐藏的 input 上,读屏工具和键盘用户会迷失方向。

所以接下来把它封装成组件,同时补齐这些能力。

4. 封装 React 可复用组件:props、状态机与键盘事件

React 版本的目标是做一个对外接口稳定、内部状态清晰的ClickToEditText组件。组件本身不关心数据存在哪里,只负责“查看、编辑、提交”这一个交互闭环。

4.1 对外接口设计

组件用 props 接收展示值和保存回调,父组件负责真实的持久化操作。

属性名类型默认值说明
valuestring''当前展示文本
onSave(value: string) => void提交后的保存回调
maxLengthnumber50输入最大长度
placeholderstring'点击输入内容'编辑态输入框的占位提示
emptyTextstring'点击编辑'value 为空时的展示文案
multilinebooleanfalse是否使用 textarea 多行编辑

设计要点是valueonSave分离。组件不直接改 value,而是通过 draft 维护编辑态,最终把新值交给父组件。这样父组件可以决定是否调接口、是否需要防抖、是否要记录变更历史。

4.2 组件完整实现

下面是一个可直接放到src/components/ClickToEditText.tsx的最小组件实现:

import { useRef, useState } from 'react'; export interface ClickToEditTextProps { value: string; onSave?: (value: string) => void; maxLength?: number; placeholder?: string; emptyText?: string; multiline?: boolean; } export default function ClickToEditText({ value, onSave, maxLength = 50, placeholder = '点击输入内容', emptyText = '点击编辑', multiline = false, }: ClickToEditTextProps) { const [editing, setEditing] = useState(false); const [draft, setDraft] = useState(''); const composingRef = useRef(false); const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement>(null); const textRef = useRef<HTMLSpanElement>(null); function enterEdit() { setDraft(value); setEditing(true); } function exitEditing() { setEditing(false); requestAnimationFrame(() => { textRef.current?.focus(); }); } function commit(save: boolean) { if (composingRef.current) { return; } if (!save) { exitEditing(); return; } const next = draft.trim(); if (next.length === 0) { exitEditing(); return; } if (next !== value) { onSave?.(next); } exitEditing(); } function handleKeyDown(e: React.KeyboardEvent) { if (composingRef.current) { return; } if (e.key === 'Enter' && !multiline) { e.preventDefault(); commit(true); } else if (e.key === 'Escape') { e.preventDefault(); commit(false); } } if (!editing) { return ( <span ref={textRef} className="click-to-edit-text" role="button" tabIndex={0} title={placeholder} onClick={enterEdit} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); enterEdit(); } }} > {value.trim().length > 0 ? ( value ) : ( <span className="click-to-edit-placeholder">{emptyText}</span> )} </span> ); } const commonProps = { ref: inputRef as any, value: draft, maxLength, placeholder, onChange: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => setDraft(e.target.value), onBlur: () => commit(true), onKeyDown: handleKeyDown, onCompositionStart: () => { composingRef.current = true; }, onCompositionEnd: () => { composingRef.current = false; }, style: { fontSize: 'inherit', width: multiline ? '100%' : 'auto', }, }; return multiline ? ( <textarea {...commonProps} rows={3} /> ) : ( <input {...commonProps} type="text" /> ); }

把这个组件在App.tsx里用起来:

import { useState } from 'react'; import ClickToEditText from './components/ClickToEditText'; export default function App() { const [nickname, setNickname] = useState('前端开发小张'); return ( <div style={{ padding: 24 }}> <h1>笔名</h1> <ClickToEditText value={nickname} onSave={(next) => { setNickname(next); }} /> </div> ); }

运行npm run dev,点击页面里的文本,输入新内容后按回车,文本会更新;按 Escape,内容保持不变。

4.3 关键实现点拆解

这段组件里有几个容易被忽略的设计决策:

  1. draft 和 value 必须分离。如果编辑过程中直接修改 value,用户每次输入都会触发父组件更新,Escape 取消也就失效了。
  2. commit 是唯一出口。回车、失焦、Escape 都走向 commit,只是参数不同。这样空值校验、防重复提交、焦点回收都只写一次。
  3. 中文输入法保护。composingRef只是普通 ref,不是 state,因为它的变化不需要触发渲染,只用来在事件回调里做判断。
  4. 保存回调只在内容变化时触发。next !== value防止用户什么都没改就触发一次无意义的接口调用。
  5. 查看态使用 role="button" 和 tabIndex={0},让键盘用户也可以聚焦并编辑。这个点后面可访问性章节会细说。

这里还有一个取舍:组件在空值时会退化为显示emptyText,此时不调用 onSave。这是产品策略问题,有的产品希望用户清空后保存空字符串,有的希望直接回退旧值。实际项目里应该由产品需求决定,组件代码只需要把策略固定下来即可。

5. Vue 3 组合式 API 版本,验证同一种模式可以迁移

点击编辑文字不是一个 React 专属问题。用 Vue 3 的<script setup>来实现,逻辑几乎可以一一对应,正好用对比的方式验证“状态机 + 事件边界”这套思路的可迁移性。

5.1 模板结构

Vue 组件放在src/components/ClickToEditText.vue。模板部分用v-if切换查看态和编辑态:

<template> <span v-if="!editing" class="click-to-edit-text" role="button" :tabindex="0" :title="placeholder" @click="enterEdit" @keydown="handleDisplayKeyDown" > <span v-if="displayText">{{ displayText }}</span> <span v-else class="click-to-edit-placeholder">{{ emptyText }}</span> </span> <input v-else ref="inputRef" type="text" v-model="draft" :maxlength="maxLength" :placeholder="placeholder" @blur="commit(true)" @keydown="handleKeyDown" @compositionstart="composing = true" @compositionend="composing = false" /> </template>

5.2 逻辑部分

脚本部分使用组合式 API 管理 draft、editing 和 composing 状态:

<script setup lang="ts"> import { computed, nextTick, ref } from 'vue'; const props = withDefaults( defineProps<{ value: string; onSave?: (value: string) => void; maxLength?: number; placeholder?: string; emptyText?: string; }>(), { maxLength: 50, placeholder: '点击输入内容', emptyText: '点击编辑', } ); const editing = ref(false); const draft = ref(''); const composing = ref(false); const inputRef = ref<HTMLInputElement | null>(null); const displayText = computed(() => props.value.trim()); async function enterEdit() { draft.value = props.value; editing.value = true; await nextTick(); inputRef.value?.focus(); inputRef.value?.select(); } function exitEditing() { editing.value = false; } function commit(save: boolean) { if (composing.value) { return; } if (!save) { exitEditing(); return; } const next = draft.value.trim(); if (next.length === 0) { exitEditing(); return; } if (next !== props.value) { props.onSave?.(next); } exitEditing(); } function handleKeyDown(e: KeyboardEvent) { if (composing.value) { return; } if (e.key === 'Enter') { e.preventDefault(); commit(true); } else if (e.key === 'Escape') { e.preventDefault(); commit(false); } } function handleDisplayKeyDown(e: KeyboardEvent) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); enterEdit(); } } </script>

使用方式和 React 版本类似:

<template> <ClickToEditText :value="nickname" :on-save="handleSave" /> </template> <script setup lang="ts"> import { ref } from 'vue'; import ClickToEditText from './components/ClickToEditText.vue'; const nickname = ref('前端开发小张'); function handleSave(next: string) { nickname.value = next; } </script>

5.3 React 与 Vue 实现的差异点对比

两套代码最终行为几乎一致,差异只是框架语法,不是交互模型。

对比项React 写法Vue 写法
条件切换JSX 中if (!editing)返回不同元素template 中v-if / v-else
输入绑定value={draft}+onChangev-model="draft"
事件绑定React 合成事件onKeyDownDOM 事件@keydown
DOM 获取useRefref+nextTick
props 默认值解构赋值withDefaults

如果后续要支持 textarea 多行编辑,Vue 版本在 input 标签上加一个v-if / v-else分支即可,逻辑不需要改动。

6. 边界场景、中文输入法与无障碍处理

组件能跑通基础流程只是第一步。真实项目里,决定体验好坏的是那些边界场景:输入为空怎么办、中文输入法怎么处理、键盘用户能不能用。

6.1 空值、空白字符与长度限制

空值和纯空格的处理策略需要一开始就定好。上面组件采用的方式是:提交前trim(),结果为空的直接回退到查看态,不调用 onSave。这样展示区不会出现一个看似空白实则占位的奇怪区域。

长度限制通过maxLength传给原生输入控件。要注意的是maxLength计算的是 JavaScript 字符串的 UTF-16 code unit 数量,不是按“用户看起来的字符数”计算的。表情符号、生僻字可能占两个 code unit,所以在做字符数统计和剩余字数提示时,不能直接拿maxLength的差值当作剩余字数,需要结合Array.from(value)Intl.Segmenter重新计算。

另一个常见坑是:maxLength 在中文输入法组合过程中也可能提前截断。用户正在输入拼音,输入法是按字母逐个进入输入框的,如果拼音长度超过 maxLength,组合过程会被打断。要彻底解决,需要先移除maxLength,在 composition 结束后手动截断 draft。

6.2 中文输入法为什么容易误提交

中文输入法输入“你好”时,键盘事件顺序是:用户按下 n、i,浏览器触发 compositionstart;之后输入的拼音字母处于组合状态;用户按回车选字,此时触发 compositionend,并把“你”写入输入框。如果在组合状态下直接监听 keydown 的 Enter,就会在选字时误判成“提交编辑”。

解决办法就是组件里的composing标志位:

  • compositionstart时标记 true。
  • compositionend时标记 false。
  • keydown 事件里,如果 composing 为 true,直接 return。

只有组合结束后的 Enter 才被认为是确认提交。同样,Escape 也可以被输入法组合过程触发,所以也需要相同的保护。这个细节在英文环境测试时永远不会暴露,但中文、日文、韩文输入法下几乎必现。

6.3 键盘可达性与焦点管理

很多点击编辑组件只考虑了鼠标操作,键盘用户完全无法使用。改进目标是:不依赖鼠标也能完成“聚焦文本、进入编辑、修改、提交”的完整流程。

查看态文本要能获得焦点。实现方式是给 span 加上role="button"tabIndex={0},并监听 Enter 和空格键进入编辑。进入编辑后,输入框自动聚焦并全选文本;按 Escape 或提交后,焦点再次回到文本区域,这样键盘用户的焦点不会丢失。

如果要在读屏软件里提供更明确的提示,可以给查看态文本加上aria-label="点击编辑",当前内容为 xxx"。对于真实项目,至少要做到:键盘 Tab 能到达文本、Enter 能进入编辑、Escape 能无副作用退出。这三个能力是按键可达性的最低标准。

7. 常见问题排查:从现象到根因

点击编辑组件虽然小,但出问题时现象往往非常隐蔽。下面这些问题是真实项目里最容易遇到的。

7.1 问题排查总表

问题现象可能原因检查方式处理建议
点击后输入框一闪而过事件冒泡触发外层重新渲染,或 blur 被立即触发在点击元素上打印事件触发链路阻止冒泡,或确认外层没有 React key 变化
中文输入法按回车直接提交未处理 composition 事件输入拼音后按回车观察行为组合期间跳过 commit
失焦后保存按钮点击不触发blur 先于 click 执行在按钮 click 里打日志用 onMouseDown 的 preventDefault 阻止失焦,或 setTimeout 延迟提交
保存后还是旧值draft 与 value 未分离打印 draft 和 value 的当前值编辑态使用独立 draft
内容保存后多了空格未做 trim在 commit 前打印原始字符串提交前统一 trim
表格单元格编辑后宽度错乱查看态和输入框宽度不一致检查父子容器宽度给输入框设置 min-width 或使用 ch 单位

7.2 点击后输入框一闪而过

现象是:鼠标一按下,输入框出现,随即又消失,根本来不及输入。

最常见原因是点击 span 后,事件冒泡到某个外层容器,外层容器触发了重新渲染或用 React 的 diff 把当前组件替换掉,导致编辑态被重置。排查时先看组件的 key 是否稳定,再看外层是否包裹了会重渲染的列表项。解决方式通常是在enterEdit里对点击事件调用e.stopPropagation(),或者确认外层组件不会因本次点击改变 key。

7.3 中文输入法回车误提交

现象是:用户输入拼音,按空格或回车选字,结果编辑直接提交了,输入框关闭,用户还没选完字。

根因就是前面讲的 composition 问题。检查方法是在 keydown 里打日志,观察按下回车时e.isComposing是否为 true。React 合成事件里可以直接用e.nativeEvent.isComposing,更稳妥的方式是自己维护 compositionstart/end 标志位,也就是组件中composingRef的做法。

7.4 blur 和按钮点击的时序冲突

很多产品会在输入框旁边放一个“确定”按钮。正常点击按钮时,浏览器先触发输入框的 blur,再触发按钮的 click。如果 blur 事件里已经执行了 commit(true),保存关闭编辑态,后面的 click 可能拿不到正确的上下文,导致二次保存或者保存提前关闭。

推荐的解决方案是:保存按钮不用 click,而用onMouseDown里调用e.preventDefault()。preventDefault 会阻止输入框失焦,click 事件依然可以正常触发,但组件的 blur 提交逻辑不会在这个点击链路里被执行。或者,在 blur 的回调里用一个小延迟,比如setTimeout(commit, 100),给 click 事件留出执行窗口。第二种方式简单,但要小心延迟保存带来的重复提交问题。

8. 生产环境落地清单与扩展方向

演示组件可以直接跑通开发环境,但进生产之前,还有一些工程能力需要补齐。

8.1 学习环境不够,生产环境还要补的能力

开发环境里 onSave 只是更新内存 state,生产环境通常是调用接口。接口场景下要考虑:

  • 保存中的 loading 状态:在保存完成前禁用再次进入编辑,防止重复提交。
  • 接口失败回滚:接口返回异常时,把展示值恢复为旧 value,并给出错误提示。
  • 防抖:连续编辑多个字段时,不要在每次回车都立刻发请求,可以延迟提交。
  • 埋点:记录进入编辑、提交成功、取消等事件,后续才能评估交互使用率。
  • 国际化:placeholder、emptyText、错误提示都要纳入多语言管理。
  • 样式隔离:组件样式不要用全局 class 污染页面,建议使用带前缀的 class 名或 CSS Modules。

组件本身不感知接口,它只是把“新的值”交给父组件。真正负责请求、错误处理、回滚的是父组件,这个职责边界要清楚。

8.2 上线前检查清单

可以把这个清单直接作为需求验收标准:

检查项检查内容通过标准
交互完整性点击、Enter、Escape、blur 四类操作每一步表现符合预期
输入校验空值、纯空格、超长、特殊字符空值回退,超长截断,无脏数据
中文输入法拼音组合过程中按回车不提交,组合结束后才提交
可访问性Tab 可达、焦点回收、语义提示不依赖鼠标也能完成编辑
异步保存请求失败、慢请求、快速重复提交失败回滚,loading 防重复
样式查看态和编辑态宽度、字体一致性切换过程无明显跳动

8.3 常见扩展方向

点击编辑文字只是一个基础模式,理解了它之后,可以继续扩展:

  • 表格单元格编辑:把组件放进表格,配合行数据索引实现单行保存。
  • 多行自适应:textarea 根据内容高度自动撑开,避免出现滚动条。
  • 乐观更新:保存接口先返回成功,界面立即更新,失败时回滚并提示。
  • 富文本编辑:当需求升级到加粗、颜色、链接时,再引入成熟的富文本编辑器。

建议初学者不要把重点放在封装多少 props 上,而是先手工把原生版本的事件边界写一遍,再用框架实现一版。点击编辑文字这个功能,难度不在 API,而在你能否说清楚“什么时候进编辑态、什么时候提交、什么时候取消、输入法组合期间怎么办”。把这四件事想明白,无论换 Vue、React 还是小程序,都能很快写出同样的组件。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询