早期为了解决“会话保持”的需求,社区中出现了「cookie方案」并最终成为W3C标准:当某个网站登录成功后,客户端(浏览器)收到一个cookie标识(文本)并保存下来,在后续请求中会自动带上这个字段,由此Web后台可以判断是否同一个用户,从而使“会话”得以延续。
微信小程序 没有像浏览器一样内置实现了cookie方案,需要开发者自行模拟,而原先京东购物小程序 及京喜小程序(现微信一级购物入口)是从微信及手Q购物H5中迁移迭代出来的,也就是说我们不仅要在小程序中模拟一套cookie方案,并且要保持和原业务对cookie处理逻辑的一致,为此我们将实现方向确定为“基于小程序开放能力,和浏览器保持一致”。
微信小程序 开放了 数据缓存 Storage 和 网络 Network 这两种能力,通过这两套API,我们可以自行DIY一个cookie方案。
PS:本文所有代码及使用示例都可以 在这里 找到,阅读本文时配合实践,效果更佳。
二、浏览器中的cookie
为了保持后端对cookie的处理逻辑和原来的H5一致,小程序的实现需要往浏览器看齐。
所以模拟小程序的cookie前,先看看浏览器的cookie机制,主要有以下几个部分:
本地存储:浏览器会在本地分配一块空间,存储cookie
请求携带:每次发起请求,都会从本地取出cookie并追加在请求头上
响应设置:当响应头有Set-Cookie字段时,需要解析并更新
过期时间:每个cookie字段有单独的过期时间,并且到期会自动清除
读写操作:暴露API给前端JS调用,可进行增删改查操作
作用域:路径path、域名domin
编码:cookie值,在网络传输需要encode,建议存储也一样
其它:HttpOnly、Secure、SameSite
在浏览器的 DevTools
中,可以看到当前站点下的Cookie明细:
三、小程序中的cookie实现
方案设计
在小程序中模拟Cookie,主要涉及五个部分:
其中我们会重点关注 「Cookie基础库」 的实现,另外也会给出「Request基础库」的封装示例。
本地存储
小程序提供了 「数据缓存 Storage API」(可以理解为Web规范中的 LocalStorage
),支持存储“原生类型、Date、及能够通过JSON.stringify序列化的对象”。
我们可以利用这些API,在Storage中新开一个 cookies
字段进行存储:
wx .setStorageSync ('cookies' , cookies)
wx .getStorageSync ('cookies' )
复制代码
其中 cookies
的「存储结构」如下:
{
cookie1 : {
name : 'cookie1' ,
value : 'xxx' ,
expires : 'Fri, 17 Jan 2020 08:49:41 GMT'
}
},
复制代码
上面的 cookie1
便是一个“最小cookie单元 cookieItem
”,包含了3个字段(name、value、expires),是本文中定义的「标准cookie格式」,也是cookie操作的基本单元。
打开【微信开发工具】的 Storage
选项卡,可以查看本地存储的情况:
读写操作
这部分主要作为“公共基础库“的角色,为外部业务提供增删改查cookie的API。
1. 获取cookie———— getCookie()
步骤:从Storage中取出完整cookies ==> 取出指定name的cookie项 ==> 校验有效期 ==> 返回值value
实现如下:
function getCookie (name = '' ) {
let cookies = wx.getStorageSync('cookies' )
let { value, expires } = cookies[name] || {}
return (name && expires && !isExpired(expires)) ? decodeURIComponent (cookieItem.value) : ''
}
复制代码
2. 设置cookie———— setCookie()
步骤:从Storage中取出完整cookies ==> 解析入参 ==> 覆盖更新 ==> 同步到本地Storage
首先看下本API设计需求:
设置单个/多个cookie
直接传值/传cookieItem(Object)
时间格式maxAge/expires
调用示例如下:
setCookie ({
cookie1 : 12345 ,
cookie2 : '12345'
})
setCookie ({
cookie1 : {
value : 12345 ,
maxAge : 3600 * 24
},
cookie2 : {
value : '12345' ,
expires : 'Wed, 21 Oct 2015 07:28:00 GMT'
}
})
复制代码
这里可对入参遍历,而cookie子项无论直接传值value还是传了详细object,都尽量的获取 name/value/expires/maxAge
,传给格式化函数转为标准的 cookieItem
:
function setCookie (cookiesParam ) {
let oldCookies = wx.getStorageSync('cookies' )
let newCookies = {}
for (let name in cookiesParam) {
if (isObject(cookiesParam[name])) {
let { value, expires, maxAge } = cookiesParam[name]
newCookies[name] = getStandardCookieItem({ name, value, expires, maxAge })
} else {
newCookies[name] = getStandardCookieItem({ name, value : cookiesParam[name] })
}
}
saveCookiesToStorage(Object .assign({}, oldCookies, newCookies))
}
复制代码
3. 删除cookie———— removeCookie()
步骤:从Storage中取出完整cookies ==> 删除指定的cookie项 ==> 同步到本地Storage
function removeCookie (cookieName ) {
let cookies = wx.getStorageSync('cookies' )
delete cookies[cookieName]
saveCookiesToStorage(Object .assign({}, cookies))
}
复制代码
四、Cookie 在网络中的传递
本节主要简单实现设计图中的【Request基础库】部分
如上图所示,Cookie在网络中的传输主要有四个过程:
Set -Cookie
Cookie
Cookie
以下是对一个请求的抓包示例:
在小程序中,请求发起有两种方式: HTTP
和 WebSocket
,这里以HTTP为例,先对请求api进行「封装」:
function requestPro ({ url, data, header, method = 'GET' } ) {
return new Promise ((resolve, reject ) => {
wx.request({
url,
data,
header : Object .assign({}, { 'Cookie' : CookieLib.getCookiesStr() }, header),
success (res) {
let { data : resData, header, statusCode } = res
let setCookieStr = header['Set-Cookie' ] || header['set-cookie' ] || ''
CookieLib.setCookieFromHeader(setCookieStr)
resolve(resData)
},
fail (err) {
reject(err)
}
})
})
}
复制代码
如上代码所示,Cookie在前端侧请求模块中的处理主要有3点:
1. 请求携带
步骤:(每次发请求前)从Storage中取出完整cookies ==> 转化为HTTP规范的请求头Cookie格式 ==> 设置到 Request Header
中
上面代码中的 getCookiesStr()
直接取cookies拼接即可,返回示例: cookie1=xxx;cookie2=yyy
。
2. 响应设置
步骤:(每次收到响应后)解析 Response Header
的 Set-Cookie
字段 ==> 转为标准Cookie格式 ==> setCookie()
这里处理 Set-Cookie
内容时,有几个点需要留意: - 最基本的格式: Set-Cookie: <cookie-name>=<cookie-value>
- 可能同时包含多个cookie字段,以,分割(但需要排除时间值里的,) - 时间格式:Max-Age/Expires (不区分大小写)
具体实现可在文末Demo中找到。
3. 编码问题
「Cookie值编码方式」是容易产生困惑的地方,目前看到的广泛做法都是使用「URL编码」。
但笔者翻阅 RFC6265 发现,原始规范中并没有对编码进行指定,比如在第四章 Server Requirements (服务端)中是这样描述:
To maximize compatibility with user agents, servers that wish to store arbitrary data in a cookie-value SHOULD encode that data, for example, using Base64 [RFC4648].
“为了最好的兼容效果,服务端应该对cookie值进行编码,例如使用Base64。”
而在第五章 User Agent Requirements (客户端,也就是浏览器),则是“建议以第四章服务端的实现为准”。
总之规范并没有指定使用「URL编码」,但基于该编码方案已经深入人心,也就顺其自然成了“默认选择”。那这里也不做例外,浏览器怎么做,咋们小程序也保持一致。
在浏览器中,推荐cookie值经过 encode
编码后保存下来,所以直接取到的也是 encode
后的值,所以追加在请求头 Cookie
字段,就不需要 decode
解码了,直接拼接即可(但基础库API的get操作最终需要进行 decode
解码)。
而对于响应头 Set-Cookie
的值,我们认为后端已经做了 encode
编码,所以前端不需要处理,直接存进 Storage 即可。
五、性能优化(高频读写)
前面实现中每次读写cookie都会调用小程序Storage API(而且是同步的),小程序框架会读写到本地Storage。 对于高频场景,可以将cookie在内存中维护一份,读写都直接走「内存层」,有更新才同步到「Storage层」。
1. 初始化
首先需要在内存中声明一个 _COOKIES
(命名自行diy),建议在cookie基础库中声明,便于统一维护。
2. 读
前面初始化时已经从Storage读取一次cookies,后续getCookie就直接读内存的 _COOKIES
即可。
3. 写
写操作直接更新内存,间接更新Storage。 如果有高频写场景,可以考虑做个任务队列进行节流。
六、单元测试
微信官方在2019年5月推出了「小程序自动化 SDK」 miniprogram-automator
,经过半年多的迭代,目前已基本稳定下来。
在购物小程序场景试用了一下,cookie相关的用例很快就完成了,简直是开发者的福音:真香!!!
实际项目中,对cookie的单元测试可以分为两类:
小程序全局范围的cookie验证(比如初始化小程序后,有没有种下版本号、访问行为等关键cookie)
cookie基础库API验证(比如get/set/remove等各个API是否正常工作)
以验证 setCookie()
API为例:
it('API验证:setCookie()' , async () => {
await miniProgram.evaluate(() => {
wx.CookieLib.setCookie({
cookie1: 12345 ,
})
})
邀请
原作者: 模板之家
来自: 网络收集