JavaScript奇技淫巧:利用Cookie实现一个可记忆位置的拖拽小方块


JavaScript奇技淫巧:利用Cookie实现一个可记忆位置的拖拽小方块

在 Web 开发中,我们常常需要"记住"用户的一些状态或偏好,比如网站的主题颜色、一个可拖动窗口的位置,或者一个折叠面板是否展开。这些看似微小的功能,却能极大地提升用户体验。今天,我们就来探讨实现这一"记忆"功能的经典技术------Cookie,并亲手打造一个能记住自己位置的拖拽元素。

在动手之前,我们必须先理解 Cookie 是什么,以及它为何存在。

为什么需要 Cookie?

HTTP 协议本身是无状态(Stateless)的。这意味着服务器不会记住你上一次的请求。你刷新一下页面,服务器就"忘了"你是谁。为了解决这个问题,Cookie 应运而生。它就像是浏览器随身携带的一张"身份证",每次访问一个网站时,都会自动带上这张身份证,服务器就能通过它认出你。

简单来说,Cookie 就是一小段存储在用户浏览器上的文本数据,它与特定的域名绑定。

通过 document.cookie,我们可以在客户端用 JavaScript 来读写 Cookie。但它的操作方式有些"古怪",更像是在追加字符串,而不是操作一个对象。一个完整的 Cookie 字符串不仅仅是 key=value,它还可以包含多个属性,用分号隔开,来控制其行为。

下面是 Cookie 的核心属性详解:

  1. name=value (键值对) 这是 Cookie 的主体。name 是唯一的标识符。

    javascript 复制代码
    // 设置一个最简单的 cookie
    document.cookie = 'username=kaivon';
    // 注意:再次赋值是添加或覆盖,而不是替换整个 cookie 字符串
    document.cookie = 'skin=blue'; 
    // 此时 document.cookie 的值可能是 "username=kaivon; skin=blue"
  2. expires (过期时间) 设置一个具体的 GMT 格式的日期和时间。一旦过了这个时间点,浏览器就会自动删除这个 Cookie。

    javascript 复制代码
    // 设置一个在2030年1月1日过期的 cookie
    document.cookie = 'user_id=123; expires=' + new Date('2030-01-01').toUTCString();
  3. max-age (有效期) 一个更现代的属性,用来设置 Cookie 从创建开始可以存活的秒数

    • 正数:表示存活的秒数。
    • 0:立即删除该 Cookie。
    • 负数或不设置:表示这是一个"会话 Cookie",当浏览器窗口关闭时,它就会被删除。
    javascript 复制代码
    // 设置一个有效期为1周的 cookie
    document.cookie = 'token=xyz; max-age=' + (60 * 60 * 24 * 7);
  4. domain (有效域) 指定了哪些域名可以访问这个 Cookie。默认情况下,Cookie 只属于创建它的那个域名。

    javascript 复制代码
    // 这个 cookie 只有在 a.example.com 及其子域名(如 b.a.example.com)下有效
    document.cookie = 'data=something; domain=a.example.com';
  5. path (有效路径) 指定了域名下哪个路径可以访问 Cookie。默认是 /,即整个域名下都有效。

    javascript 复制代码
    // 这个 cookie 只有在 /docs 及其子路径下才会被发送
    document.cookie = 'doc_id=456; path=/docs';
  6. Secure 一个布尔标记。如果带上 Secure 属性,那么这个 Cookie 只有在通过 HTTPS 协议请求时才会被发送到服务器。

  7. HttpOnly 一个布尔标记。如果带上 HttpOnly 属性,那么这个 Cookie 将无法通过 JavaScript (document.cookie) 访问。这是一个重要的安全措施,可以有效防止跨站脚本(XSS)攻击者窃取用户的 Cookie。

  8. SameSite 用于防止跨站请求伪造(CSRF)攻击。它可以设置为 Strict, Lax, 或 None,用来控制 Cookie 是否应该在跨域请求中被发送。

小结 :原生操作 document.cookie 非常繁琐且容易出错,尤其是在读取和解析特定值时。因此,在实际项目中,我们通常会封装一个 Cookie 管理工具。

Part 2: 实战!打造一个"有记忆"的拖拽元素

理论讲完了,让我们来点有趣的。我们将创建一个可以随意拖动的 <div> 小方块,并利用 Cookie 让它在刷新页面后,依然能"记得"自己上次被拖放的位置。

1. HTML 和 CSS 结构

首先,我们需要一个简单的 HTML 结构和一些 CSS 样式来定义我们的小方块。

html 复制代码
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Draggable Box with Memory</title>
    <style>
        body {
            height: 100vh;
            margin: 0;
            overflow: hidden; /* 防止拖动时出现滚动条 */
        }
        #box {
            width: 100px;
            height: 100px;
            position: absolute;
            left: 100px; /* 初始位置 */
            top: 100px;
            background: #409EFF;
            cursor: move;
            user-select: none; /* 防止拖动时选中文本 */
            color: white;
            display: flex;
            justify-content: center;
            align-items: center;
            font-family: sans-serif;
        }
    </style>
</head>
<body>
    <div id="box">Drag Me!</div>
    <script>
        // JavaScript 代码将放在这里
    </script>
</body>
</html>

为了避免直接操作繁琐的 document.cookie 字符串,我们先创建一个 CookieManager 类来简化操作。

javascript 复制代码
class CookieManager {
    set(name, value, maxAgeSeconds = 3600 * 24 * 7) { // 默认保存7天
        // 使用 encodeURIComponent 对 value 进行编码,防止特殊字符导致 cookie 格式错误
        const encodedValue = encodeURIComponent(value);
        document.cookie = `${name}=${encodedValue}; max-age=${maxAgeSeconds}; path=/`;
    }

    get(name) {
        // 将 cookie 字符串分割成数组
        const cookies = document.cookie.split('; ');
        for (const cookie of cookies) {
            // 分割键和值
            const [key, value] = cookie.split('=');
            if (key.trim() === name) {
                // 找到后,解码并返回
                return decodeURIComponent(value);
            }
        }
        return null; // 未找到则返回 null
    }

    remove(name) {
        // 通过将 max-age 设置为 0 来删除 cookie
        this.set(name, '', 0);
    }
}

这个管理器提供了清晰的 set, get, remove 方法,并且自动处理了值的编解码,非常方便。

3. 实现拖拽逻辑 Draggable

现在是核心部分。我们将创建一个 Draggable 类,它负责处理所有的拖拽逻辑和 Cookie 存储。

javascript 复制代码
class Draggable {
    constructor(element) {
        if (!element) return;
        this.dom = element;
        this.cookie = new CookieManager();
        this.init();
    }

    init() {
        // **记忆功能核心**:尝试从 Cookie 读取上次保存的位置
        const left = this.cookie.get('boxLeft');
        const top = this.cookie.get('boxTop');

        // 如果 Cookie 中有值,就应用它
        if (left !== null && top !== null) {
            this.dom.style.left = `${left}px`;
            this.dom.style.top = `${top}px`;
        }

        // 绑定鼠标按下事件,启动拖拽
        this.dom.onmousedown = this.mouseDown.bind(this);
    }

    mouseDown(e) {
        // 1. 计算鼠标指针与元素左上角的偏移量
        this.disX = e.clientX - this.dom.offsetLeft;
        this.disY = e.clientY - this.dom.offsetTop;

        // 2. 为了避免鼠标移动过快飞出元素导致事件丢失,
        //    将 mousemove 和 mouseup 事件绑定到 document 上
        this.onMouseMove = this.mouseMove.bind(this);
        this.onMouseUp = this.mouseUp.bind(this);

        document.addEventListener('mousemove', this.onMouseMove);
        document.addEventListener('mouseup', this.onMouseUp);
    }

    mouseMove(e) {
        // 3. 根据鼠标的实时位置和偏移量,计算出元素的新位置
        const newLeft = e.clientX - this.disX;
        const newTop = e.clientY - this.disY;

        this.dom.style.left = `${newLeft}px`;
        this.dom.style.top = `${newTop}px`;
    }

    mouseUp() {
        // 4. 鼠标抬起,拖拽结束
        // 解绑 document 上的事件,防止不必要的性能消耗
        document.removeEventListener('mousemove', this.onMouseMove);
        document.removeEventListener('mouseup', this.onMouseUp);

        // **记忆功能核心**:将当前元素的最终位置存入 Cookie
        this.cookie.set('boxLeft', this.dom.offsetLeft);
        this.cookie.set('boxTop', this.dom.offsetTop);
    }
}

4. 启动!

最后,我们只需要获取 DOM 元素并实例化 Draggable 类即可。

javascript 复制代码
// 在 script 标签的末尾添加:
const box = document.getElementById('box');
new Draggable(box);

现在,打开你的 HTML 文件,随意拖动那个蓝色的小方块,然后刷新页面。你会惊喜地发现,它完美地停留在了你上次松开鼠标的位置!

完整代码

html 复制代码
<!DOCTYPE html>
<html>

<head>
	<meta charset="UTF-8">
	<title></title>
	<style>
		#box {
			width: 100px;
			height: 100px;
			position: absolute;
			left: 100px;
			top: 100px;
			background: green;
		}
	</style>
</head>

<body>
	<div id="box"></div>
	<script>
		// 更健壮的 Cookie 管理器
		class CookieManager {
			set(name, value, maxAgeSeconds) {
				const encodedValue = encodeURIComponent(value);
				document.cookie = `${name}=${encodedValue}; max-age=${maxAgeSeconds}; path=/`;
			}

			get(name) {
				const cookies = document.cookie.split('; ');
				for (const cookie of cookies) {
					const [key, value] = cookie.split('=');
					if (key.trim() === name) {
						return decodeURIComponent(value);
					}
				}
				return null; // 返回 null 表示未找到
			}

			remove(name) {
				this.set(name, '', 0);
			}
		}

		// 使用 class 封装拖拽逻辑
		class Draggable {
			constructor(element) {
				if (!element) return;
				this.dom = element;
				this.cookie = new CookieManager();
				this.init();
			}

			init() {
				// 从 Cookie 或计算样式中恢复位置
				const left = this.cookie.get('boxLeft');
				const top = this.cookie.get('boxTop');

				if (left !== null && top !== null) {
					this.dom.style.left = `${left}px`;
					this.dom.style.top = `${top}px`;
				}

				this.dom.onmousedown = this.mouseDown.bind(this);
			}

			mouseDown(e) {
				this.disX = e.clientX - this.dom.offsetLeft;
				this.disY = e.clientY - this.dom.offsetTop;

				// 绑定到 document 上,并保存引用以便移除
				this.onMouseMove = this.mouseMove.bind(this);
				this.onMouseUp = this.mouseUp.bind(this);

				document.addEventListener('mousemove', this.onMouseMove);
				document.addEventListener('mouseup', this.onMouseUp);
			}

			mouseMove(e) {
				const newLeft = e.clientX - this.disX;
				const newTop = e.clientY - this.disY;

				// 使用 transform 提升性能
				// this.dom.style.transform = `translate(${newLeft - this.dom.offsetLeft}px, ${newTop - this.dom.offsetTop}px)`;
				// 如果仍要用 left/top,则用下面这行
				this.dom.style.left = `${newLeft}px`;
				this.dom.style.top = `${newTop}px`;
			}

			mouseUp() {
				// 解绑事件
				document.removeEventListener('mousemove', this.onMouseMove);
				document.removeEventListener('mouseup', this.onMouseUp);

				// 每次都保存最新的位置,解决了"只点击不移动"的 bug
				this.cookie.set('boxLeft', this.dom.offsetLeft, 3600 * 24 * 7); // 保存7天
				this.cookie.set('boxTop', this.dom.offsetTop, 3600 * 24 * 7);

			}
		}

		// 启动
		const box = document.getElementById('box');
		new Draggable(box);
	</script>
</body>

</html>

总结与思考

通过这个小项目,我们不仅复习了 Cookie 的基础知识,还通过一个实用的 CookieManager 类学会了如何优雅地管理它。更重要的是,我们亲手实现了一个结合 DOM 事件和数据持久化的有趣功能。

我们可以更进一步吗? 当然!

  • 替代方案 :对于这种纯客户端的状态存储,localStorage 是一个更现代、API 更友好、存储容量也更大的选择(通常为 5MB)。它不会像 Cookie 那样在每次 HTTP 请求中都发送到服务器,因此更适合这种场景。你可以尝试用 localStorage.setItemlocalStorage.getItem 来替换 CookieManager 的逻辑。
  • 性能优化 :在 mouseMove 事件中,频繁修改 lefttop 会导致浏览器不断重绘(Repaint)和回流(Reflow)。对于追求极致性能的动画,使用 transform: translate(x, y) 会有更好的表现,因为它通常能利用 GPU 加速。

希望这篇从理论到实践的文章,能帮助你更深入地理解 Cookie,并激发你创造更多有趣的用户体验!

相关推荐
andwhataboutit?21 小时前
LANGGRAPH
java·服务器·前端
无限大621 小时前
为什么"Web3"是下一代互联网?——从中心化到去中心化的转变
前端·后端·程序员
cypking21 小时前
CSS 常用特效汇总
前端·css
程序媛小鱼21 小时前
openlayers撤销与恢复
前端·js
Thomas游戏开发21 小时前
如何基于全免费素材,0美术成本开发游戏
前端·后端·架构
若梦plus21 小时前
Hybrid之JSBridge原理
前端·webview
chilavert31821 小时前
技术演进中的开发沉思-269 Ajax:拖放功能
前端·javascript·ajax
xiaoxue..21 小时前
单向数据流不迷路:用 Todos 项目吃透 React 通信机制
前端·react.js·面试·前端框架
有点笨的蛋21 小时前
LangChain 入门与实践:从 LLM 调用到 AI 工作流的工程化思维
前端·langchain
若梦plus1 天前
Canvas基础
前端·canvas