JavaScript奇技淫巧:利用Cookie实现一个可记忆位置的拖拽小方块
在 Web 开发中,我们常常需要"记住"用户的一些状态或偏好,比如网站的主题颜色、一个可拖动窗口的位置,或者一个折叠面板是否展开。这些看似微小的功能,却能极大地提升用户体验。今天,我们就来探讨实现这一"记忆"功能的经典技术------Cookie,并亲手打造一个能记住自己位置的拖拽元素。
Part 1: 揭开 Cookie 的神秘面纱
在动手之前,我们必须先理解 Cookie 是什么,以及它为何存在。
为什么需要 Cookie?
HTTP 协议本身是无状态(Stateless)的。这意味着服务器不会记住你上一次的请求。你刷新一下页面,服务器就"忘了"你是谁。为了解决这个问题,Cookie 应运而生。它就像是浏览器随身携带的一张"身份证",每次访问一个网站时,都会自动带上这张身份证,服务器就能通过它认出你。
简单来说,Cookie 就是一小段存储在用户浏览器上的文本数据,它与特定的域名绑定。
Cookie 的核心属性
通过 document.cookie
,我们可以在客户端用 JavaScript 来读写 Cookie。但它的操作方式有些"古怪",更像是在追加字符串,而不是操作一个对象。一个完整的 Cookie 字符串不仅仅是 key=value
,它还可以包含多个属性,用分号隔开,来控制其行为。
下面是 Cookie 的核心属性详解:
-
name=value
(键值对) 这是 Cookie 的主体。name
是唯一的标识符。javascript// 设置一个最简单的 cookie document.cookie = 'username=kaivon'; // 注意:再次赋值是添加或覆盖,而不是替换整个 cookie 字符串 document.cookie = 'skin=blue'; // 此时 document.cookie 的值可能是 "username=kaivon; skin=blue"
-
expires
(过期时间) 设置一个具体的 GMT 格式的日期和时间。一旦过了这个时间点,浏览器就会自动删除这个 Cookie。javascript// 设置一个在2030年1月1日过期的 cookie document.cookie = 'user_id=123; expires=' + new Date('2030-01-01').toUTCString();
-
max-age
(有效期) 一个更现代的属性,用来设置 Cookie 从创建开始可以存活的秒数。- 正数:表示存活的秒数。
- 0:立即删除该 Cookie。
- 负数或不设置:表示这是一个"会话 Cookie",当浏览器窗口关闭时,它就会被删除。
javascript// 设置一个有效期为1周的 cookie document.cookie = 'token=xyz; max-age=' + (60 * 60 * 24 * 7);
-
domain
(有效域) 指定了哪些域名可以访问这个 Cookie。默认情况下,Cookie 只属于创建它的那个域名。javascript// 这个 cookie 只有在 a.example.com 及其子域名(如 b.a.example.com)下有效 document.cookie = 'data=something; domain=a.example.com';
-
path
(有效路径) 指定了域名下哪个路径可以访问 Cookie。默认是/
,即整个域名下都有效。javascript// 这个 cookie 只有在 /docs 及其子路径下才会被发送 document.cookie = 'doc_id=456; path=/docs';
-
Secure
一个布尔标记。如果带上Secure
属性,那么这个 Cookie 只有在通过 HTTPS 协议请求时才会被发送到服务器。 -
HttpOnly
一个布尔标记。如果带上HttpOnly
属性,那么这个 Cookie 将无法通过 JavaScript (document.cookie
) 访问。这是一个重要的安全措施,可以有效防止跨站脚本(XSS)攻击者窃取用户的 Cookie。 -
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>
2. 封装一个更优雅的 Cookie 管理器
为了避免直接操作繁琐的 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.setItem
和localStorage.getItem
来替换CookieManager
的逻辑。 - 性能优化 :在
mouseMove
事件中,频繁修改left
和top
会导致浏览器不断重绘(Repaint)和回流(Reflow)。对于追求极致性能的动画,使用transform: translate(x, y)
会有更好的表现,因为它通常能利用 GPU 加速。
希望这篇从理论到实践的文章,能帮助你更深入地理解 Cookie,并激发你创造更多有趣的用户体验!