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,并激发你创造更多有趣的用户体验!

相关推荐
uwvwko7 小时前
buuctf——web刷题第5页
前端·python·php·web·ctf·buuctf
ldj20208 小时前
下拉默认全选,选择展示对象的字段list
前端·javascript
Lsx_8 小时前
前端数据可视化:基于Vue3封装 ECharts 的最佳实践
前端·vue.js·echarts
brzhang8 小时前
技术榜单都快刷爆了,美团的“龙猫”大模型怎么就没声了?
前端·后端·架构
小猪猪屁8 小时前
前端实时通信怎么选?HTTP、WebSocket、SSE 一文看懂
前端·websocket·http
满圆圆8 小时前
前端eslint工程化配置
前端
掘金安东尼8 小时前
React 19 发布:useTransition 平滑异步过渡!
前端·javascript·github
用户47949283569158 小时前
ESLint支持多线程Linting啦
前端·javascript·面试
不一样的少年_8 小时前
Onion CLI:3秒建项目,10秒出包的Chrome插件开发脚手架神器
前端·vue.js·chrome