从零实现一个“就地编辑”组件:仿 B站个人简介的 EditInPlace 类

从零实现一个"就地编辑"组件:仿 B站个人简介的 EditInPlace 类


最近在刷 Bilibili 的时候,注意到个人主页的「个人简介」区域有一个非常流畅的交互体验:点击文字即可直接编辑,输入完成后点击"保存"或"取消",整个过程无需跳转页面,也没有传统表单的笨重感。这种"就地编辑(Edit In Place)"的交互模式在现代 Web 应用中越来越常见。

于是我想:能不能自己动手实现一个类似的组件?不依赖任何框架,纯原生 JavaScript + 面向对象编程(OOP)的方式封装成一个可复用的类?

今天,我就带大家一步步实现这个 EditInPlace 组件,并深入探讨其设计思路与封装技巧。


一、需求分析

我们要实现的功能很简单:

  • 初始状态下显示一段文本(如用户简介);
  • 点击文本后,切换为输入框 + 保存/取消按钮;
  • 用户可编辑内容,点击"保存"则更新文本并隐藏输入框;
  • 点击"取消"则放弃修改,恢复原始内容;
  • 整个组件应具备良好的封装性,便于在不同项目中复用。

这正是典型的 就地编辑(Edit In Place) 场景。


二、设计思路:用 OOP 封装组件

为了提高代码的可维护性和复用性,我们采用 面向对象编程(OOP) 的方式,将整个功能封装成一个 EditInPlace 类。

核心要素

  • 属性:存储组件的状态和 DOM 引用;
  • 方法:负责创建 DOM、绑定事件、切换显示状态等;
  • 封装:对外只暴露必要接口,内部实现细节对使用者透明。

三、代码实现

1. HTML 结构(入口)

xml 复制代码
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>EditInPlace Demo</title>
</head>
<body>
  <div id="app"></div>
  <script src="./edit_in_place.js"></script>
  <script>
    const ep = new EditInPlace('slogan', '', document.getElementById('app'));
  </script>
</body>
</html>

我们只需提供一个挂载点(#app),然后实例化 EditInPlace 即可。


2. JavaScript 实现(核心逻辑)

kotlin 复制代码
/* edit_in_place.js */

/**
 * @func EditInPlace 就地编辑组件
 * @param {string} id - 组件唯一ID
 * @param {string} value - 初始显示文本
 * @param {HTMLElement} parentElement - 挂载的父容器
 */
function EditInPlace(id, value, parentElement) {
  this.id = id;
  this.value = localStorage.getItem(id) || value || '这是一个懒货,什么都没有留下';
  this.parentElement = parentElement;

  // DOM 引用
  this.containerElement = null;
  this.staticElement = null;   // 显示文本的 span
  this.fieldElement = null;    // 输入框 input
  this.saveButton = null;      // 保存按钮
  this.cancelButton = null;    // 取消按钮

  // 初始化
  this.createElement();
  this.attachEvent();
}

EditInPlace.prototype = {
  /**
   * 创建所有 DOM 元素并插入到页面
   */
  createElement: function () {
    this.containerElement = document.createElement('div');
    this.containerElement.id = this.id;

    // 文本显示区域
    this.staticElement = document.createElement('span');
    this.staticElement.textContent = this.value;
    this.containerElement.appendChild(this.staticElement);

    // 输入框
    this.fieldElement = document.createElement('input');
    this.fieldElement.type = 'text';
    this.fieldElement.value = this.value;
    this.containerElement.appendChild(this.fieldElement);

    // 按钮
    this.saveButton = document.createElement('input');
    this.saveButton.type = 'button';
    this.saveButton.value = '保存';
    this.containerElement.appendChild(this.saveButton);

    this.cancelButton = document.createElement('input');
    this.cancelButton.type = 'button';
    this.cancelButton.value = '取消';
    this.containerElement.appendChild(this.cancelButton);

    // 挂载到父容器
    this.parentElement.appendChild(this.containerElement);

    // 初始状态:仅显示文本
    this.convertToText();
  },

  /**
   * 切换为文本显示模式
   */
  convertToText: function () {
    this.staticElement.style.display = 'inline';
    this.fieldElement.style.display = 'none';
    this.saveButton.style.display = 'none';
    this.cancelButton.style.display = 'none';
  },

  /**
   * 切换为编辑模式
   */
  convertToField: function () {
    this.fieldElement.value = this.value; // 同步最新值
    this.staticElement.style.display = 'none';
    this.fieldElement.style.display = 'inline';
    this.saveButton.style.display = 'inline';
    this.cancelButton.style.display = 'inline';
  },

  /**
   * 绑定事件监听器
   */
  attachEvent: function () {
    // 点击文本 → 进入编辑
    this.staticElement.addEventListener('click', () => {
      this.convertToField();
    });

    // 保存
    this.saveButton.addEventListener('click', () => {
      this.save();
    });

    // 取消
    this.cancelButton.addEventListener('click', () => {
      this.cancel();
    });
  },

  /**
   * 保存修改
   */
  save: function () {
    var value = this.fieldElement.value.trim();
    // fetch 后端存储
    this.value = value;
    this.staticElement.innerHTML = value;
    localStorage.setItem(this.id, value);
    this.convertToText();
  },

  /**
   * 取消编辑,恢复原值
   */
  cancel: function () {
    this.convertToText();
  }
};

运行结果

点击"这是一个懒货,什么都没有留下"后-->

在输入框里我们可以修改我们的文本,达到类似B站简介的功能。


四、关键设计亮点

1. 状态分离清晰

  • staticElement 负责展示;
  • fieldElement 负责编辑;
  • 通过 display 控制显隐,逻辑简单可靠。

2. 事件委托 + 箭头函数

使用箭头函数确保 this 指向当前实例,避免作用域问题。

3. 高内聚低耦合

所有 DOM 操作、事件绑定、状态切换都封装在类内部,外部只需一行代码即可使用:

javascript 复制代码
new EditInPlace('bio', 'Hello World', document.body);

4. 可扩展性强

未来若需支持:

  • 后端自动保存(在 save() 中加入 fetch);
  • 输入校验;
  • 快捷键(如按 Enter 保存);
  • 多行文本(改用 <textarea>);

只需在现有结构上扩展,无需重写。


五、使用场景与延伸思考

这类组件非常适合用于:

  • 用户资料编辑(昵称、签名、地址等);
  • 后台管理系统中的表格字段快速修改;
  • 协作工具中的实时备注更新。

进阶建议

  • 将类改为 ES6 class 语法,更符合现代规范;
  • 支持传入配置项(如 placeholder、是否必填等);
  • 添加 loading 状态,提升用户体验;
  • 使用 Custom Elements 实现 Web Components,彻底解耦。

六、总结

通过这个小小的 EditInPlace 组件,我们不仅复刻了 B站简介的交互体验,更重要的是实践了 面向对象的封装思想:将复杂的 DOM 操作和状态管理隐藏在类内部,对外提供简洁、稳定的接口。

这正是优秀前端工程化的体现------让复杂留给自己,把简单留给他人

相关推荐
我叫张小白。2 小时前
Vue3 组件通信:父子组件间的数据传递
前端·javascript·vue.js·前端框架·vue3
王大宇_2 小时前
word解析从入门到出门
前端·javascript
松☆2 小时前
OpenHarmony + Flutter 多语言与国际化(i18n)深度适配指南:一套代码支持中英俄等 10+ 语种
android·javascript·flutter
Jingyou2 小时前
JavaScript 实现深拷贝
前端·javascript
编程猪猪侠2 小时前
Vue 通用复选框组互斥 Hooks:兼容 Element Plus + Ant Design Vue
前端·javascript·vue.js
凡人程序员2 小时前
搭建 monorepo 项目
前端·javascript
linda26182 小时前
说说 Map 和 Set 的区别及实际应用
前端·javascript
_一两风2 小时前
“点一下就能改”——这个功能为首富赚到了多少money?
前端·javascript
小飞侠在吗2 小时前
vue setup与OptionsAPI
前端·javascript·vue.js