1. 一次真实的前端开发经历:从URL取参到Cookie操作
事情是这样的。上周我在做一个内部运营系统的前端重构,有个页面要从列表页跳转到详情页,跳转时需要带上用户ID、来源渠道和回跳地址三个参数。后端同学直接在服务端渲染的模板里拼好了URL,类似/detail?userId=1024&from=list&backUrl=/home,我需要在纯前端的JavaScript逻辑里把这些参数安全、准确地提取出来,再根据参数值去做后面的接口请求和页面渲染。
结果一上来就踩了两个坑。第一个坑是直接用了location.search.split('&')这种最原始的方式,遇到URL编码问题直接傻了,中文参数名和特殊字符全变成了乱码。第二个坑是在详情页里需要写入一个用于标记“来源渠道”的cookie,结果发现同站策略、有效期设置、路径作用域这些细节没处理好,cookie写入后怎么取都是null,调试了大半天。最后我把这两块内容都梳理了一遍,从原理到代码到排查思路,整理成了这篇分享,希望能帮你少走弯路。
这篇文章会覆盖三个层面的内容:URL参数获取的三种常见方案及原理对比,Cookie操作的完整封装(设置、读取、删除、属性详解),以及我实际开发中遇到的典型问题和排查实录。适合刚入门前端、或者对浏览器API细节还不太熟的同学,也欢迎老手帮我挑挑刺。
先说明一下,正文里所有代码都是基于原生JavaScript和浏览器标准API,不依赖任何框架。如果你想直接用Vue、React里的现成工具库,文末我也会给几句选型建议。
2. URL携带参数获取:三种方案横向拆解
2.1 最原始的正则大法:优点和缺点同样明显
第一个方案,正则匹配。这是一种“裸奔”式的写法,核心思路就是拿到window.location.search(问号后面的部分),然后用正则把key=value的结构抽出来。
function getQueryParam(key) { const search = window.location.search.substring(1); const reg = new RegExp('(^|&)' + key + '=([^&]*)(&|$)'); const match = search.match(reg); if (match) { return decodeURIComponent(match[2]); } return null; }这段代码的关键点有两个。第一个是(^|&)用来匹配参数名的起始位置,确保userId不会被adminUserId这种带前缀的key误匹配到。第二个是decodeURIComponent对提取到的值做解码,不然中文会变成%E4%B8%AD%E6%96%87这种编码串。
但说实话,这个方案在现如今的开发里已经不太推荐了。原因有三:一,它无法处理同一个key出现多次的情况,比如?tag=a&tag=b,正则只会匹配到第一个;二,URL编码逻辑脆弱,如果参数值里有&或=的编码字符,处理起来会出各种幺蛾子;三,维护性差,代码可读性不高,换个同事来看得琢磨半天。赶时间的时候可以这么写,但要长期维护的话真心不建议。
2.2 现代浏览器原生API:URLSearchParams 的正确打开方式
第二个方案是浏览器原生提供的URLSearchParamsAPI。这是目前我首推的方式,代码简洁,语义清晰,兼容性也完全够用。
function getQueryParams() { const params = new URLSearchParams(window.location.search); return Object.fromEntries(params.entries()); } // 用法 const params = getQueryParams(); console.log(params.userId); // '1024' console.log(params.from); // 'list' console.log(params.backUrl); // '/home'如果你不只想要一次性获取所有参数,还可以针对单个key操作:
const params = new URLSearchParams(window.location.search); const userId = params.get('userId'); // 取第一个 const allTags = params.getAll('tag'); // 取全部,返回数组 console.log(params.has('from')); // 是否存在URLSearchParams 的好处不言而喻:原生支持URL编码解码、原生支持重复key、原生支持迭代。比如之前那个?tag=a&tag=b,用getAll('tag')就能拿到['a', 'b'],这才是符合预期的行为。
有一点要特别提醒:URLSearchParams 在解析时会自动将+号解码为空格。这个行为是遵循application/x-www-form-urlencoded规范的,如果你传入的参数值里本身就带有+(比如加密签名),就要注意这一层转换。我在后文的排查部分会专门再讲一次。
2.3 框架生态里的现成方案:什么时候用、怎么选
第三个方案,是用第三方工具库或框架自带的方法。比如经常和URL打交道的qs、query-string这两个npm包,在一些老项目里很常见。
用qs的时候是这样:
import qs from 'qs'; const parsed = qs.parse(window.location.search, { ignoreQueryPrefix: true }); console.log(parsed.userId); // '1024'query-string的用法也类似。这类库的优势是解析深度嵌套对象、数组时的表现更好,比如?filter[name]=tom&filter[age]=18这种结构,原生API基本没法优雅处理,但qs能直接解析成一个嵌套对象。缺点是引入了依赖,对于一个简单的location.search解析场景,性价比不算高。
如果是Vue Router环境,官方推荐用route.query直接拿;React Router里也有对应的useSearchParams。这些本质上都是对 URLSearchParams 的封装,日常开发完全够用。
我的选型建议很简单:纯前端页面用 URLSearchParams,Vue项目里用 route.query,React项目里用 useSearchParams,只有遇到极其复杂的嵌套查询参数时再上 qs 这种重型工具。不为一个能用一行代码解决的需求引入几百KB的依赖。
3. Cookie操作全流程:从原理到封装
3.1 先把Cookie的“脾气”摸清楚:属性与作用域
Cookie这个东西,本质上就是浏览器存储在本地的一小段文本数据,每次请求同一个域名时会自动携带。它有几个关键属性,弄清楚这几个属性,你就能理解为什么cookie有时候写入失败,有时候读出来是null。
| 属性 | 作用 | 日常建议 |
|---|---|---|
| name/value | cookie的名称和取值 | 名称不要用中文,取值建议编码 |
| expires / max-age | 有效期,不设置默认是会话级(关闭浏览器即失效) | 持久化登录建议设置 expires 或 max-age |
| path | 生效路径,默认是当前路径,可设置为/全局生效 | 一般业务统一用/ |
| domain | 生效域名,默认当前域名,不包含子域名 | 需要跨子域时显式设置 |
| secure | 仅HTTPS协议下发送 | 生产环境务必开启 |
| samesite | 跨站请求时是否携带,可设 Strict / Lax / None | 防御CSRF,建议Lax |
| httponly | 是否允许document.cookie读取 | 敏感数据建议交给后端设置 |
这里稍微解释下samesite,这是很多前端同学容易忽视的属性。如果你在跨站的场景下没设置SameSite=None并且同时要求Secure,现代浏览器大概率会拒绝这个cookie的写入。也就是说,如果你在HTTP环境下测试SameSite=None; Secure,那cookie根本存不进去,因为Secure属性要求HTTPS。我在排查部分还会提到。
3.2 手写一套可直接复用的Cookie封装(设置、读取、删除全过程)
基于上面的属性理解,我封装了一套日常业务通用的cookie工具。这段代码我用了两年多,在多个项目中稳定运行,你可以直接抄走。
const CookieUtil = { /** * 设置cookie * @param {string} name 键名 * @param {string} value 键值 * @param {Object} options 可选参数 * @param {number} options.maxAge 有效秒数,不传则关闭浏览器失效 * @param {string} options.path 路径,默认 '/' * @param {string} options.domain 域名 * @param {boolean} options.secure 是否仅https * @param {string} options.sameSite Lax / Strict / None */ set(name, value, options = {}) { const { maxAge, path = '/', domain, secure = false, sameSite = 'Lax' } = options; let cookieStr = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`; if (maxAge) { cookieStr += `; max-age=${maxAge}`; } if (path) { cookieStr += `; path=${path}`; } if (domain) { cookieStr += `; domain=${domain}`; } if (secure) { cookieStr += '; secure'; } if (sameSite) { cookieStr += `; samesite=${sameSite}`; } document.cookie = cookieStr; }, /** * 根据键名读取cookie */ get(name) { const target = encodeURIComponent(name); const cookies = document.cookie.split('; ').map(item => item.split('=')); for (let i = 0; i < cookies.length; i++) { const [key, value] = cookies[i]; if (key === target) { return decodeURIComponent(value); } } return null; }, /** * 删除cookie */ remove(name, options = {}) { const { path = '/', domain } = options; let cookieStr = `${encodeURIComponent(name)}=; max-age=0`; if (path) { cookieStr += `; path=${path}`; } if (domain) { cookieStr += `; domain=${domain}`; } document.cookie = cookieStr; } };这里有一个细节值得展开:设置cookie时建议对名称和值都做encodeURIComponent。为什么?因为cookie的值里如果包含中文、分号、空格,可能导致整个cookie字符串解析错乱。比如你存一个value='hello; world',不加编码的话,后续读取时大概率会把字符串拆成两段。我在实际项目中就遇到过一次用户昵称含特殊字符导致cookie读取异常的情况,当时排查了快一个小时才发现是这个原因。
读取和删除的逻辑是全套封装里的灵魂。get方法先把document.cookie按;分割,再逐个取等号左侧的键名做匹配,最后对值做decodeURIComponent。remove方法利用了cookie的覆盖机制:把过期时间设为过去(max-age=0),浏览器就会立即清除这个cookie。但注意,删除时的path、domain必须和设置时保持一致,不然是删不掉的。
3.3 一个完整的业务场景:从URL取参后写入来源cookie
把前面两块内容串起来,模拟一个真实场景:用户从列表页点击进入详情页,URL携带from参数,前端读取这个参数,写入一个cookie,标记用户的来源渠道,有效期30天。
// 1. 从URL中读取来源渠道 function getSourceFromUrl() { const params = new URLSearchParams(window.location.search); return params.get('from') || 'direct'; } // 2. 将来源写入cookie,有效期30天 function trackSource() { const source = getSourceFromUrl(); CookieUtil.set('source_channel', source, { maxAge: 30 * 24 * 60 * 60, path: '/', sameSite: 'Lax' }); } // 3. 调用 trackSource();这个流程看起来很简单,但背后的设计意图值得说两句。为什么用max-age而不是expires?因为max-age是相对秒数,设置起来更直观,不需要去计算一个具体的过期时间点。为什么path要设成/?因为如果不设,cookie只在当前路径及其子路径下生效,比如你从/list页面写的cookie,在/detail页面可能取不到——这是很多“为什么cookie读出来是null”的经典原因之一。
4. 实战中的坑与排查记录:我遇到的问题和解决思路
4.1 URL参数提取的经典翻车现场
我在实际开发中至少遇到三次 URL 参数解析相关的bug,每次都是细节问题。
第一个是“+号变空格”问题。有一次做活动页,URL里带了一个base64编码的参数,base64的字符集里包含+,结果用 URLSearchParams 解析后,+变成了空格,导致后续的解码直接失败。这个问题的根源在于URLSearchParams遵循表单编码规范,+本身就代表空格。如果是自己拼接URL,强烈建议用encodeURIComponent对参数值做编码,而不是直接裸拼。
第二个是中文乱码。旧代码里用location.search.split('&')拿到参数后直接就用,没有做decodeURIComponent,结果中文活动标题在页面上显示成%E6%...一串乱码。这类问题在交接老项目时格外常见,看到这类代码直接重写吧。
第三个是hash路由导致的读取失败。在Vue Router的hash模式下,URL长这样:http://example.com/#/detail?userId=1024。这时候location.search是空的(因为问号在hash后面),要拿参数得用window.location.hash去解析。这个问题在切换路由模式的场景下会有两个人来问一次。
4.2 Cookie操作中的经典疑难杂症
Cookie相关的坑更多,我挑三个最典型的。
第一个是跨域或跨子域拿不到cookie。比如你的接口域名是api.example.com,前端页面是www.example.com,如果cookie写在api.example.com下,前端document.cookie是读不到的。解决方法是设置domain=.example.com,让cookie在主域名下生效。但注意,如果是完全跨域(a.com和b.com),cookie方案基本不可行,只能用其他方案。
第二个是SameSite策略导致的cookie写入失败。我在详情页给第三方嵌入系统设置cookie时,怎么写都不生效。检查后发现系统是第三方的iframe嵌入,请求是跨站的,cookie需要设置SameSite=None; Secure才能被接收。但测试环境是HTTP,Secure又会导致写入失败,这就陷入了“必须HTTPS才能跨站存cookie”的尴尬局面。最后只能让后端配合在网关层处理,前端方案在HTTP环境下无解。
第三个是手动修改cookie 不生效。有时候在chrome DevTools里手动改cookie,刷新页面后又变回去了。这不是前端代码问题,而是cookie的作用域、时效性在浏览器内部有一套自己的管理逻辑。排查时建议先在Network面板看请求头里的Cookie字段,确认cookie是否真的带上了,再回到代码里找原因。
4.3 适用于初学者的常见问题速查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 取不到URL里的参数 | 参数在hash部分而非search部分 | 打印location.search和location.hash |
| URL参数中文乱码 | 未做decodeURIComponent | 统一使用URLSearchParams解析 |
| 参数值里的+号变成空格 | URLSearchParams表单编码规则 | 对参数值做encodeURIComponent |
| cookie写入后读取为null | path/domain不一致 | 检查设置和读取时的path、domain |
| cookie能写入但刷新消失 | 未设置过期时间,默认会话级 | 设置max-age或expires |
| 跨站场景下cookie失效 | SameSite=None 未生效或非HTTPS | 检查SameSite和Secure属性 |
| 第三方页面cookie无法读取 | HttpOnly为true | 让后端在当前域下写入cookie |
这张表我每次给团队新人培训都会放出来,基本覆盖了日常开发中90%的URL参数和cookie操作异常。
5. 从实操到工程化:代码封装的一些进阶建议
5.1 不要重复造轮子,但要造好轮子
如果你所在项目里已经用了成熟的工具库,比如axios里有处理cookie的扩展,或者公司内部的公共方法库已经封装好了URL解析,那优先用现成的。但如果公司内部没有、也不打算引入第三方库,上面这套代码可以直接当作团队的公共模块使用。
有一个很现实的问题:团队成员水平不一,有人喜欢直接document.cookie = 'a=1',有人会用封装好的工具。我的建议是在团队规范里明确规定,所有cookie操作一律走统一封装,不允许在业务代码里裸写document.cookie。这样后续做安全加固(比如统一加SameSite属性)时,只需要改一个文件,而不是全局搜索所有裸写的cookie代码。
5.2 一个常见误区:不要用cookie存敏感数据
最后必须强调一点安全常识。cookie会随着每次HTTP请求自动发送到服务器,明文传输时很容易被截获。也不要用cookie存用户密码、token等敏感信息,万一被XSS脚本读取到document.cookie,数据就彻底暴露了。
作为前端,我们能做的是:
- 非必要不存敏感信息到cookie,优先考虑
sessionStorage或localStorage(但也各有风险,需要结合业务场景评估); - 必须存cookie时设置好
HttpOnly(这个一般由后端设置)、SameSite=Lax或Strict; - 对于用户输入的URL参数,一定要做编码校验后再使用,防止被注入恶意标记。
6. 写在最后的实操复盘
这次从URL取参到cookie操作的过程,让我重新把浏览器底层的几个API梳理了一遍。有些同学可能觉得这些内容太基础,但恰恰是这些基础细节,最容易在线上出问题。实测下来,URLSearchParams 配合我自己封装的 CookieUtil,代码量少、逻辑集中、排查方便,比之前东拼西凑的写法稳健得多。建议你现在就可以打开控制台,拿你的业务URL跑一遍,看看有没有踩到上面提到的某个坑。
如果你在项目里也遇到了类似的取参或cookie问题,欢迎留言告诉我具体场景,我们可以一起讨论更优雅的解法。