likes
comments
collection
share

Bilibili 主页个性签名:就地编辑功能的实现就地编辑的用户体验 首先,我们可以来参考在B站的个人主页中,它的个性签

作者站长头像
站长
· 阅读数 26

作为一名优秀的前端开发工程师,我们在与用户进行交互时,通常要把用户体验放在第一位,就地编辑(EditInPlace)作为一种高级用户体验设计,在现代Web应用中变得越来越普遍。那今天我们就来实战一下关于主页的个性签名中,我们可以怎么去实现就地编辑的功能呢?

就地编辑的用户体验

首先,我们可以来参考在B站的个人主页中,它的个性签名的编辑方式;如下图所示

Bilibili 主页个性签名:就地编辑功能的实现就地编辑的用户体验 首先,我们可以来参考在B站的个人主页中,它的个性签

通常情况下,用户在页面上看到的是文本状态,比如个人签名。当用户将鼠标移动到该区域时,文本会自动切换到编辑状态,显示一个输入框供用户修改内容。编辑完成后,用户退出编辑状态,系统自动保存更改并将输入框切换回文本状态。这种就地编辑的效果就大大地提升了用户的体验感

实现就地编辑功能

像就地编辑这种功能,我们不仅仅可以把它用在个性签名上面,很多地方我们都可以用它来提升用户体验,对此我们应该进行功能函数的封装,来增强它的复用性

EditInPlace 功能函数的封装

先来看,我们用原生的 js 是怎么去封装这个类的;

// 一个文件一个类
/**
 * @func 就地编辑
 * @param {ele} container 挂载点
 * @author 
 * @date 2024/06/25
 */
// es6 2015 简洁优雅 好读 
// es6 默认值 
function EditInPlace(storageKey,container, value = '这个家伙很懒,什么都没有留下'){
    this.storageKey = storageKey;
    this.container = container;
    this.value = value;
    // 将动态创建文本和编辑input的dom 封装,代码的管理好
    this.createElement();
    this.attachEvents();
}

EditInPlace.prototype = {
    // 就地编辑需要的动态DOM
    createElement: function(){
        // DOM树
        // 创建一个div
        this.editElement = document.createElement('div');
        // 添加一个子元素
        this.container.appendChild(this.editElement);
        
        // signature 文本值
        this.staticElement = document.createElement('span');
        this.staticElement.innerHTML = this.value;
        this.editElement.appendChild(this.staticElement);

        // input
        this.fieldElement = document.createElement('input');
        this.fieldElement.type = 'text';
        this.fieldElement.value = this.value;
        this.editElement.appendChild(this.fieldElement);

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

        // 取消按钮
        this.cancelButton = document.createElement('input')
        this.cancelButton.type = 'button';
        this.cancelButton.value = '取消';
        this.editElement.appendChild(this.cancelButton);

        // 初始化文本状态
        this.converToText()
    },
    // 文本状态
    converToText:function(){
        this.staticElement.style.display = 'inline';
        this.fieldElement.style.display = 'none';
        this.saveButton.style.display = 'none';
        this.cancelButton.style.display = 'none';
    },
    // 编辑状态
    converToEdit: function(){
        this.staticElement.style.display = 'none';
        this.fieldElement.style.display = 'inline';
        this.saveButton.style.display = 'inline';
        this.cancelButton.style.display = 'inline';
    },
    // 事件监听
    attachEvents: function(){
        // this
        // let that = this;
        // this.staticElement.addEventListener('click',function(){
        //     // this 指向元素
        //     // this 丢失了
        //     that.converToEdit();
        // })
        this.staticElement.addEventListener('click',() => {
            this.converToEdit();
        })
        this.saveButton.addEventListener('click',() => {
            this.save()
        })
        this.cancelButton.addEventListener('click',() => {
            this.converToText()
        })
    },
    save: function(){
        this.value = this.fieldElement.value;
        this.staticElement.innerHTML = this.value
        this.converToText();
        // html5 localStorage
        localStorage.setItem(this.storageKey,this.value);
        this.saveData();
    },
    saveData: function(){
        let value = this.value
        // restful = url 定义方式 + method
        // GET 读 POST 创建 PUT 更新 PATCH 局部更新 DELETE 删除
        // 看到这个URL 就知道啥资源?
        // 修改资源 GET
        fetch('http://localhost:3000/users/1',{
            method: 'PATCH',
            // 请求头,发送的内容 格式是json
            headers: {
                'Content-Type': 'application/json'
            },
            // 请求体
            body: JSON.stringify({
                signature: value
            })
        })
        .then(res => res.json())
        .then(data => {
            console.log(data,'保存成功');
        })

    }
}

在上面的代码中,我们封装了EditInPlace类,并且给它传入了storageKey(本地存储键名),container(挂载点)和 value(文本值),且给value设置了默认值,'这个家伙很懒,什么都没有留下',然后还调用了其原型上的创建DOM树的createElement()和节点的事件监听attachEvents()方法

初始化 DOM 树结构

createElement 方法可以帮我们去初始化需要的DOM树结构

  1. 首先为需要添加就地编辑功能的节点 container 创建并添加一个div子元素 editElement
  2. 然后把接下来会用到的DOM结构都挂载到子元素 editElement 上;
  3. 比如文本状态下的文本值用 span 标签创建的 staticElement 节点
  4. 编辑状态下的输入框 input 创建的 fieldElement 节点
  5. 以及确定(saveButton)和取消(cancelButton)按钮
  6. 添加完所有DOM结构之后,再通过 converToText() 方法去初始化文本状态

将 DOM 树设为文本状态

converToText() 方法就是把初始化的DOM树让它显示成文本下的状态,即只有文本值,输入框等会被隐藏

        this.staticElement.style.display = 'inline';
        this.fieldElement.style.display = 'none';
        this.saveButton.style.display = 'none';
        this.cancelButton.style.display = 'none';

只将文本值的节点 staticElementdisplay 属性设置为 'inline',而其他的DOM节点都将 display 属性设置为 'none'

将 DOM 树设为编辑状态

同样的,我们可以通过 converToEdit 方法把初始化的DOM树让它显示成编辑时的状态

        this.staticElement.style.display = 'none';
        this.fieldElement.style.display = 'inline';
        this.saveButton.style.display = 'inline';
        this.cancelButton.style.display = 'inline';

对节点进行事件监听

接下来我们还要去对节点进行事件的监听 attachEvents

        this.staticElement.addEventListener('click',() => {
            this.converToEdit();
        })
        this.saveButton.addEventListener('click',() => {
            this.save()
        })
        this.cancelButton.addEventListener('click',() => {
            this.converToText()
        })

因为我们在初始化 DOM 树时就已经进行了将 DOM 树设为文本状态的操作

  • 所以我们在监听文本值的节点 staticElement 时,当文本被点击就会调用 converToEdit() 方法将 DOM 树设为编辑状态
  • 而编辑状态下,当点击确定按钮 saveButton,就会通过 save() 方法把新签名保存到后端的数据中
  • 当点击取消按钮 cancelButton 时,再次调用 converToText() 方法把 DOM 树再次设回文本状态

注意,在 addEventListener 事件监听函数中,巧妙地使用了ES6中的箭头函数,正因为箭头函数中没有this关键字这个机制,致使箭头函数内部的this是继承自外部的,所以才能访问到converToEdit()方法;如果是普通函数this 的值也会指向触发事件的元素,就访问不到converToEdit()方法了。

保存新签名到本地和后端

来看在 save 方法中,需要进行哪些操作;

    save: function(){
        this.value = this.fieldElement.value;
        this.staticElement.innerHTML = this.value
        this.converToText();
        // html5 localStorage
        localStorage.setItem(this.storageKey,this.value);
        this.saveData();
    }

首先要获取在输入框中的新签名,并将其赋给文本节点staticElement;再调用converToText()方法把 DOM 树再次设回文本状态;并且更新本地存储 localStorage 中的签名值,再通过 saveData 方法,保存数据到后端

    saveData: function(){
        let value = this.value
        // restful = url 定义方式 + method
        // GET 读 POST 创建 PUT 更新 PATCH 局部更新 DELETE 删除
        // 看到这个URL 就知道啥资源?
        // 修改资源 GET
        fetch('http://localhost:3000/users/1',{
            method: 'PATCH',
            // 请求头,发送的内容 格式是json
            headers: {
                'Content-Type': 'application/json'
            },
            // 请求体
            body: JSON.stringify({
                signature: value
            })
        })
        .then(res => res.json())
        .then(data => {
            console.log(data,'保存成功');
        })
    }

PATCH 是 HTTP 协议中的一种请求方法,用于对资源进行局部更新

EditInPlace 类的封装

在 ES6 中,打造了 class 关键字给我们更加便捷地去封装一个类,我们来看 class 是怎么封装EditInPlace 类的;

// 为了让js更好地适合大型企业级开发,原型式的面向对象 升级为传统面向对象
// 拥抱更多其他语言程序员
// class extend implement oo 能力用出来
// 虽是es5没有的class,本质任然是原型式的,只是语法糖

class EditInPlace {
    constructor(storageKey, container, value = '这个家伙很懒'){
        this.storageKey = storageKey;
        this.container = container;
        this.value = value;
        // 将动态创建文本和编辑input的dom 封装,代码的管理好
        this.createElement();
        this.attachEvents();
    }
    createElement(){
        // DOM树
        // 创建一个div
        this.editElement = document.createElement('div');
        // 添加一个子元素
        this.container.appendChild(this.editElement);
        
        // signature 文本值
        this.staticElement = document.createElement('span');
        this.staticElement.innerHTML = this.value;
        this.editElement.appendChild(this.staticElement);

        // input
        this.fieldElement = document.createElement('input');
        this.fieldElement.type = 'text';
        this.fieldElement.value = this.value;
        this.editElement.appendChild(this.fieldElement);

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

        // 取消按钮
        this.cancelButton = document.createElement('input')
        this.cancelButton.type = 'button';
        this.cancelButton.value = '取消';
        this.editElement.appendChild(this.cancelButton);

        // 初始化文本状态
        this.converToText()
    }
    attachEvents(){
        this.staticElement.addEventListener('click',() => {
            this.converToEdit();
        })
        this.saveButton.addEventListener('click',() => {
            this.save()
        })
        this.cancelButton.addEventListener('click',() => {
            this.converToText()
        })
    }
    // 文本状态
    converToText(){
        this.staticElement.style.display = 'inline';
        this.fieldElement.style.display = 'none';
        this.saveButton.style.display = 'none';
        this.cancelButton.style.display = 'none';
    }
    // 编辑状态
    converToEdit(){
        this.staticElement.style.display = 'none';
        this.fieldElement.style.display = 'inline';
        this.saveButton.style.display = 'inline';
        this.cancelButton.style.display = 'inline';
    }
    save(){
        this.value = this.fieldElement.value;
    }
    get value(){
        console.log('111');
        return this.value;
    }
    set value(value){
        this.value = value;
        this.staticElement.innerHTML = this.value
        this.converToText();
        // html5 localStorage
        localStorage.setItem(this.storageKey,this.value);
    }
}

ES6 的 Class

虽然原型式面向对象(Prototype-based)是JavaScript的强大特性之一,但对于习惯于其他编程语言的开发者而言,这种方式可能不够直观。因此,ES6引入了class语法糖,使JavaScript的面向对象编程风格更接近于传统的面向对象语言,如Java或C#。这种语法不仅提高了代码的可读性,还简化了类的定义和继承

启动后端服务

在实现前后端交互时,使用RESTful API是现代Web开发的常见做法。一个网站中的一切皆可视为资源,例如用户信息、帖子数据等。通过RESTful API,可以使用标准的HTTP动词(如GET、POST、PUT、DELETE)对资源进行操作。

json-server

在我们的实现中,使用json-server可以快速创建一个模拟的RESTful API。通过定义db.json文件,我们可以模拟一个简单的用户数据资源,并通过URL访问这些资源。例如:

  • http://localhost:3000/users:获取所有用户的集合。
  • http://localhost:3000/users/1:获取特定用户的信息。
npm init -y   // 初始化  
npm i json-server   // 安装 json-server
npm run dev json-server --watch db.json   // 启动 json-server 并监听 db.json 文件

db.json

{
  "users": [
    {
      "id": "1",
      "name": "旅梦码客",
      "signature": "12321"
    },
    {
      "id": "2",
      "name": "剑行天下",
      "signature": "搞钱"
    }
  ]
}

Bilibili 主页个性签名:就地编辑功能的实现就地编辑的用户体验 首先,我们可以来参考在B站的个人主页中,它的个性签

设计前端页面

前端页面如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <!-- 定义文档的字符编码为 UTF-8 -->
    <meta charset="UTF-8">
    <!-- 确保页面在不同设备上的显示效果一致 -->
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <!-- 定义文档的标题为就地编辑 -->
    <title>就地编辑</title>
</head>
<body>
    <!-- 定义一个id为signature的div元素,用于显示和编辑签名 -->
    <div id="signature"></div>
    <!-- 引入edit_in_place.js文件,该文件包含了EditInPlace类的定义 -->
    <script src="./edit_in_place.js"></script>
    <script>
    // 定义存储签名的本地存储键名
    const STORAGEKEY = "signature";
    
    // 发起一个GET请求,获取id为1的用户的签名数据
    fetch("http://localhost:3000/users/1")
    // 等待请求完成,并将响应数据转换为JSON格式
   .then(res => res.json())
    // 等待JSON数据解析完成,并获取其中的签名数据
   .then(data => {
    // 提取签名数据
    const { signature } = data;
    // 从本地存储中获取已存储的签名
    const stoSignature = localStorage.getItem(STORAGEKEY);

    // 如果服务器返回的签名与本地存储的签名不一致
    if (signature!== stoSignature) {
        // 将服务器返回的签名更新到本地存储中
        localStorage.setItem(STORAGEKEY,signature);
    }
    // 创建一个新的EditInPlace实例,传入本地存储键名、签名显示的DOM元素以及初始签名值
    new EditInPlace(STORAGEKEY,document.getElementById('signature'), signature)
    })
    </script>
</body>
</html>

首先给后端发一个 GET 请求,拿到后端数据,如果后端数据中的签名和本地存储 localStorage 中的签名的值不一致,那就把本地存储 localStorage 中的签名同步成和后端数据一致,并且把页面上的挂载点以及本地的存储键名和签名值传入 EditInPlace 类给签名显示的DOM元素加上就地编辑的功能

小结

B站的就地编辑功能充分利用了JavaScript的面向对象编程、事件循环机制以及现代Web开发的最佳实践。通过封装类和使用ES6的特性,开发者不仅可以提高代码的复用性和可维护性,还能确保应用在不同的场景下表现出色。随着Web技术的不断发展,类似的高级用户体验设计将成为未来Web应用的重要组成部分

转载自:https://juejin.cn/post/7407384172048711731
评论
请登录