共计 5392 个字符,预计需要花费 14 分钟才能阅读完成。
这篇文章最初发表在我自己搭建的站点 js-cookie 库源码学习
背景
最近在做项目的时候,前端登录功能要做一个记住密码的功能。但开发用的框架中没有实现这个功能,所以我就想自己实现这个功能。实现起来其实很简单,就是每次用户在登录页面点击登录时,把用户输入的用户名和密码保存到 cookie 中就可以了,当用户再登录时,再从 cookie 中获取用户名和密码填充到表单中就可以了。当然,也可以选择保存在 localStorage 中,不过本文主要是想分析下用到的 js-cookie 这个轻量级的 js 库。
Document.cookie
Document.cookie 这个 API 比较 ” 简陋 ”。这里先简单的介绍下 Document.cookie 这个 API,主要是写入、读取、删除三个操作。
// 1. 写入 cookie, 会追加到当前的 cookie 中 | |
document.cookie = 'username=abc' | |
// 2. 读取 cookie,可以用正则匹配或者字符串解析查找的方法来读取 | |
var usename = /username=(\S+);/.exec(document.cookie)[1] // 这里只是简单的匹配了下,实际开发中要考虑很多情况 | |
// 3. 删除 cookie,设置某个 cookie 过期即可 | |
document.cookie = 'username=; expires=Thu, 01 Jan 1970 00:00:00 GMT' | |
// 4. 判断是否有某个 cookie,其实和读取差不多 | |
/username=(\S+);/.test(document.cookie) // true 有,false 没有 |
可以看到原生的 Document.cookie 写入基本靠拼字符串,读取和判断是否有要靠正则或者字符串解析,删除则也是要拼字符串。这个操作不太方便,而且和我们平常用的 API 不太一样,比如我们常用的 Map:
var map = new Map(); | |
map.set(key, value); | |
map.get(key); | |
map.has(key); | |
map.delete(key); |
它的 api 就是对 ” 键值对 ” 这种数据结构进行操作。Document.cookie 其实也是 key=value 这种键值对结构,但是它没有提供像 map 这样的 API 接口,这显然不符合我们的“直觉”和使用习惯。
js-cookie
js-cookie 库使用起来很简单,支持向浏览器中写入、读取、删除 cookie 三种操作,API 简单也符合我们的使用习惯。那么它是如何实现这三个操作的呢?这就要分析它的源码了。
注意:这里分析的是它的 releases-v2.2.0 版本。github 上的 master 分支源码已经修改,主要是把 set 和 get 方法重构了,拆分成了两个单独的方法,releases-v2.2.0 版本 set 和 get 方法是写在一起的。下面简单分析下它的源码实现:set 和 get。remove 是 set 的一种特殊情况,增加 expires 属性即可,这里就不细说了。
set 的主要逻辑 — releases-v2.2.0 版本
function api (key, value, attributes) { | |
// set 主要逻辑 | |
if (arguments.length > 1) { | |
attributes = extend({path: '/'}, api.defaults, attributes); | |
if (typeof attributes.expires === 'number') {var expires = new Date(); | |
expires.setMilliseconds(expires.getMilliseconds() + attributes.expires * 864e+5); | |
attributes.expires = expires; | |
} | |
// We're using"expires"because"max-age" is not supported by IE | |
attributes.expires = attributes.expires ? attributes.expires.toUTCString() : ''; | |
try { | |
// 这里需要注意,用户有可能要保存 json 格式的 cookie 值,所以这里需要转化下 | |
result = JSON.stringify(value); | |
if (/^[\{\[]/.test(result)) { | |
// 如果转化成了合法的 json,则将 value 重新赋值为 json 字符串,如果不含有 { 或 [,则不是 json 字符串,也就不会走这个分支 | |
value = result; | |
} | |
} catch (e) {} | |
// 这里为什么要把 value 先编码再解码呢?,下面的 key 也是,不过 key 要解码的 unicode 值少些 | |
if (!converter.write) { | |
// 内置的编码转换 | |
// ["#", "$", "&", "+", ":", "<", ">", "=", "/", "?", "@", "[", "]", "^", "`", "{", "}", "|"] | |
value = encodeURIComponent(String(value)) | |
.replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g, decodeURIComponent); | |
} else {value = converter.write(value, key); | |
} | |
// 先编码 | |
key = encodeURIComponent(String(key)); | |
// ["#", "$", "&", "+", "^", "`", "|"] | |
// 再通过字符串替换的方式解码 | |
key = key.replace(/%(23|24|26|2B|5E|60|7C)/g, decodeURIComponent); | |
// ECMA-262 standard 规范已经不推荐用这个 escape 函数了 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/escape | |
// 而且 () 也不会被编码,所以感觉下面的这句是没有必要的 | |
key = key.replace(/[\(\)]/g, escape); | |
// 拼接其它的 cookies 属性值 | |
var stringifiedAttributes = ''; | |
for (var attributeName in attributes) {if (!attributes[attributeName]) {continue;} | |
stringifiedAttributes += ';' + attributeName; | |
if (attributes[attributeName] === true) {continue;} | |
stringifiedAttributes += '=' + attributes[attributeName]; | |
} | |
return (document.cookie = key + '=' + value + stringifiedAttributes); | |
} | |
} |
set 方法在写入 cookie 时会先对 cookie 值 encodeURIComponent 然后再 decodeURIComponent,这样可以保证存储再 cookie 中的值始终是不编码的可读性性好的字符串。
get 的主要逻辑 — releases-v2.2.0 版本
function api (key, value, attributes) { | |
// Read | |
if (!key) {result = {}; | |
} | |
// To prevent the for loop in the first place assign an empty array | |
// in case there are no cookies at all. Also prevents odd result when | |
// calling "get()" | |
var cookies = document.cookie ? document.cookie.split(';') : []; | |
var rdecode = /(%[0-9A-Z]{2})+/g; | |
var i = 0; | |
for (; i < cookies.length; i++) {var parts = cookies[i].split('='); | |
var cookie = parts.slice(1).join('='); | |
if (!this.json && cookie.charAt(0) === '"') { | |
// 没看懂为什么需要这个 if 分支 | |
cookie = cookie.slice(1, -1); // 去掉了 " " | |
} | |
try {var name = parts[0].replace(rdecode, decodeURIComponent); | |
cookie = converter.read ? | |
converter.read(cookie, name) : converter(cookie, name) || | |
cookie.replace(rdecode, decodeURIComponent); | |
if (this.json) { | |
try {cookie = JSON.parse(cookie); | |
} catch (e) {}} | |
if (key === name) { | |
result = cookie; | |
break; | |
} | |
if (!key) {result[name] = cookie; | |
} | |
} catch (e) {}} | |
return result; | |
} |
整个库暴露接口的方式 — releases-v2.2.0 版本
通过上面的 get、set 以及下面的代码可以看到整个库其实返回的就是 function api() {} 这个函数,
set、get、remove 的逻辑都写在 api 这个函数里了,所以 api 这个函数看起来很长,而且由于其中涉及到许多细节处理导致逻辑也比较复杂,那么库的作者应该是意识到了这一点,所以在 github 的 master 分支上的代码已经把 get 和 set 拆分开了,感兴趣的可以去看看 js-cookie@master。
function init(converter) {function api (key, value, attributes) {api.set = api; // Cookie.set(key, value, attributes) | |
api.get = function (key) {// Cookie.get(key) | |
return api.call(api, key); | |
}; | |
api.getJSON = function () { // Cookie.getJSON(key) | |
return api.apply({json: true}, [].slice.call(arguments)); | |
}; | |
api.defaults = {}; | |
api.remove = function (key, attributes) {// Cookie.remove(key, attributes) | |
api(key, '', extend(attributes, {expires: -1})); | |
}; | |
api.withConverter = init; // 支持自定义的编码方式 | |
// 返回的是 function api() {} | |
return api; | |
} | |
// 返回的是 function api() {} | |
return init(function () {}); | |
} |
js-cookie 库 AMD 规范和 CommonJS 规范的实现
js-cookie 声称支持 AMD 规范和 CommonJS 规范,那么它是如何支持这两种规范的呢?
;(function (factory) { | |
var registeredInModuleLoader = false; | |
if (typeof define === 'function' && define.amd) { // AMD | |
define(factory); | |
registeredInModuleLoader = true; | |
} | |
if (typeof exports === 'object') { // CommonJS | |
module.exports = factory(); | |
registeredInModuleLoader = true; | |
} | |
if (!registeredInModuleLoader) { // 浏览器 | |
var OldCookies = window.Cookies; | |
var api = window.Cookies = factory(); | |
api.noConflict = function () { | |
window.Cookies = OldCookies; | |
return api; | |
}; | |
} | |
}(function () { | |
// factory 逻辑... | |
// 返回的是 function api() {} | |
return init(function () {}); | |
}); |
其实也很简单,现在很多 js 库基本都是这么实现的。
总结
js-cookie 库的源码很简单,但是它的 v2.2.0-release 版本将 set 和 get 逻辑都写在一个方法的方式其实是不提倡这么做的。这么做需要用 if 分支来做很多过滤,会导致代码逻辑较复杂。上面也看到,暴露接口时,它用到了 function.call() 和 function.apply(),以及代码内部用到的已经不推荐使用的 arguments,这都使代码的可读性降低了。不过它里面对于 cookie 的处理过程仍然值得参考。其实 MDN 上也提供了一个轻量的处理 cookie 的库一个完整支持 unicode 的 cookie 读取 / 写入器,有时间也可以看看。
参考
MDN: Document.cookie
GitHub: js-cookie
AMD 异步模块定义
CommonJS