本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
1.学习准备工作
1.1学习资料
https://github1s.com/js-cookie/js-cookie/blob/HEAD/src/api.mjs
https://github.com/js-cookie/js-cookie
https://github.com/haixiangyan/my-js-cookie
1.2 环境准备
clone 代码:https://github.com/js-cookie/js-cookie.git
安装依赖: npm i (好几次才成功)
2.为什么需要封装的cookie包
通过浏览器的开发者工具看到cookie的存储视图是非常清晰的:
但是实际当我们查看document.cookie的时候,发现其实cookie是使用字符串存储的:
并且js原生cookie的api是非常简陋的,例如创建,修改,删除要这样来操作:
// 创建
document.cookie="username=John Doe";
// 修改
document.cookie="username=John Smith; expires=Thu, 18 Dec 2043 12:00:00 GMT; path=/";
// 删除 删除 cookie 非常简单。您只需要设置 expires 参数为以前的时间即可
document.cookie = "username=; expires=Thu, 01 Jan 1970 00:00:00 GMT";
更多关于原生js cookie内容可以参看相关资料。通过以上内容我们看到当我们要修改cookie值的时候或者删除cookie的时候要进行复杂的字符串或者数组操作,如果能够把这些过程封装起来就很方便开发了。
3.了解js-cookie的使用方式
js-cookie的readme文档对齐介绍如下:
一个用于处理cookie的简单、轻量级JavaScript API:
适用于所有浏览器
接受任何字符
严峻考验
无依赖
支持ES模块
支持AMD/CommonJS
符合RFC6265标准
有用的维基
允许自定义编码/解码
<800字节gzip!
看完这个介绍,感觉太美好了,兼容性好,没有其他依赖,对js各种模块化方案都支持,体积还小,爱了。
下面看一下基本的使用:
import Cookies from 'js-cookie'
Cookies.set('foo', 'bar')
Cookies.set('name', 'value')
Cookies.set('name', 'value', { expires: 7 })
Cookies.get('name') // => 'value'
Cookies.remove('name')
4.猜测js-cookie的实现方法
1.首先,cookie项目彼此之间是使用分号(;)分隔的, 内部是使用等号(=) 分隔的。所以要用到split('; ')获取每一项,要用到split("=") 获取每一项的键和值。
2.其次,设置值的时候可以指定过期时间,所以这里要有对时间的处理逻辑
3.最后,由于允许接收任何字符和允许自定义的编码和解码,所以实现的时候要考虑到对特殊字符的处理,可能用到正则表达式;可能会用到编码和解码的api。
5.阅读代码
5.1 paclage.json文件
打开js-cookie的源码,最关心的文件不外乎是package.json 和 src目录下的index.js文件。
首先浏览一下package.json文件,值得关注的是scripts有哪些,这个包都用了什么依赖:
{
"scripts": {
"test": "grunt test",
"format": "grunt exec:format",
"dist": "rm -rf dist/* && rollup -c",
"release": "release-it"
},
"devDependencies": {
"browserstack-runner": "^0.9.0",
"eslint": "^7.31.0",
"eslint-config-standard": "^16.0.3",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-html": "^6.0.0",
"eslint-plugin-markdown": "^2.2.0",
"grunt": "^1.0.4",
"grunt-compare-size": "^0.4.2",
"grunt-contrib-connect": "^3.0.0",
"grunt-contrib-nodeunit": "^4.0.0",
"grunt-contrib-qunit": "^3.1.0",
"grunt-contrib-watch": "^1.1.0",
"grunt-exec": "^3.0.0",
"gzip-js": "^0.3.2",
"prettier": "^2.3.2",
"qunit": "^2.9.3",
"release-it": "^14.10.0",
"rollup": "^2.0.0",
"rollup-plugin-filesize": "^9.1.1",
"rollup-plugin-license": "^2.5.0",
"rollup-plugin-terser": "^7.0.2",
"standard": "^16.0.3"
}
}
确实像readme中所说的,js-cookie没有使用额外的依赖,这里只有devDependencies而没有dependencies。另外看到scripts中的命令使用了grunt和rollup,还有release-it, 感觉他们也是值得研究的。
5.2 index.js文件
再看看index.js :
module.exports = require('./dist/js.cookie')
就一句话啊,引入打包后的dist目录下的js.cookie.js文件,而现在我们还没有dist这个目录,一会儿要打包才能生成。先看src下面的源码吧~
5.3 src下面的文件
5.3.1 .mjs文件介绍
映入眼帘的是三个文件,但是文件后缀我木有见过啊~ mjs 后缀文件是什么?
.mjs 就是表示当前文件用 ESM 的方式进行加载,如果是普通的 .js 文件,则采用 CJS 的方式加载。
详细参见:
5.3.2 init方法概览
简单浏览3个文件,发现api.js里面定义了核心逻辑并且应用了另外两个文件,这另外两个文件属于提供了工具方法。
如上图所示api.mjs引入了另外两个文件,主要定义了一个init方法,并且默认导出调用了init方法的调用结果。
init方法接收两个参数,一个转换器,一个默认的属性对象。
下面看一下init方法大概的内部实现:
在init方法内部,定义了set,get方法,并返回了一个使用Object.create方法创建的对象,我们逐一看一下~
5.3.3 set方法
function set (name, value, attributes) {
// 当前环境不存在document则返回
if (typeof document === 'undefined') {
return
}
// 合并attributes (init方法的defaultAttributes和set方法的attributes)
attributes = assign({}, defaultAttributes, attributes)
// 判断过期时间是不是number类型
if (typeof attributes.expires === 'number') {
// 过期时间转成毫秒值
attributes.expires = new Date(Date.now() + attributes.expires * 864e5)
}
if (attributes.expires) {
// 转成UTC格式
attributes.expires = attributes.expires.toUTCString()
}
// cookie项名字的处理
name = encodeURIComponent(name)
.replace(/%(2[346B]|5E|60|7C)/g, decodeURIComponent)
.replace(/[()]/g, escape)
// 字符串化的属性
var stringifiedAttributes = ''
// 依次检查属性
for (var attributeName in attributes) {
// 如果属性值为假值则进入下一次检查
if (!attributes[attributeName]) {
continue
}
// 拼接属性名
stringifiedAttributes += '; ' + attributeName
if (attributes[attributeName] === true) {
continue
}
// Considers RFC 6265 section 5.2:
// ...
// 3. If the remaining unparsed-attributes contains a %x3B (";")
// character:
// Consume the characters of the unparsed-attributes up to,
// not including, the first %x3B (";") character.
// ...
// 拼接属性值
stringifiedAttributes += '=' + attributes[attributeName].split(';')[0]
}
// 给document.cookie赋值
return (document.cookie =
name + '=' + converter.write(value, name) + stringifiedAttributes)
}
特殊说明:
1.时间计算说明:1天24小时,1小时60分钟,1分钟60秒:
246060 = 86400
再转成毫秒*1000:
最终结果就是864*10^5 科学表示就是864e5
2.转成UTCString的原因:www.cnblogs.com/ajaemp/p/14…
3.replace函数:
stringObject.replace(regexp/substr,replacement)
字符串 stringObject 的 replace() 方法执行的是查找并替换的操作。它将在 stringObject 中查找与 regexp 相匹配的子字符串,然后用 replacement 来替换这些子串。如果 regexp 具有全局标志 g,那么 replace() 方法将替换所有匹配的子串。否则,它只替换第一个匹配子串。更多信息可以参考文档。
5.3.4 get方法
function get (name) {
// 没有document或者参数列表为空并且没传name,则返回
if (typeof document === 'undefined' || (arguments.length && !name)) {
return
}
// To prevent the for loop in the first place assign an empty array
// in case there are no cookies at all.
// 是否有cookie,有则使用;分隔,没有则赋值为空数组
var cookies = document.cookie ? document.cookie.split('; ') : []
// jar存储键值对形式的数据
var jar = {}
for (var i = 0; i < cookies.length; i++) {
// 对于每一项以=分隔得到键和值
var parts = cookies[i].split('=')
var value = parts.slice(1).join('=')
try {
var found = decodeURIComponent(parts[0])
jar[found] = converter.read(value, found)
if (name === found) {
break
}
} catch (e) {}
}
// 传name返回某一项,没传则返回所有的
return name ? jar[name] : jar
}
5.3.5 return 的逻辑
return Object.create(
{
set: set,
get: get,
remove: function (name, attributes) {
set(
name,
'',
assign({}, attributes, {
expires: -1
})
)
},
withAttributes: function (attributes) {
return init(this.converter, assign({}, this.attributes, attributes))
},
withConverter: function (converter) {
return init(assign({}, this.converter, converter), this.attributes)
}
},
{
attributes: { value: Object.freeze(defaultAttributes) },
converter: { value: Object.freeze(converter) }
}
)
这里使用Object.create方法创建了一个对象,这个方法接收两个参数:第一个为新创建对象的原型对象;第二个为自身定义的属性。
在原型对象上定义了set、get、remove,还有withAttributes和withConverter。remove方法的实现是通过调用set方法设置过期时间来实现的;withAttributes和withConverter则把自己的属性和传入的参数进行合并。
5.3.6 assign方法
只定义了一个参数,剩下的都用argumets来接收,后面的覆盖前面的
export default function (target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i]
for (var key in source) {
target[key] = source[key]
}
}
return target
}
5.3.7 defaultConverter
导出了一个具有read和write方法的对象,主要是调用了decodeURIComponent和encodeURIComponent
export default {
read: function (value) {
if (value[0] === '"') {
value = value.slice(1, -1)
}
return value.replace(/(%[\dA-F]{2})+/gi, decodeURIComponent)
},
write: function (value) {
return encodeURIComponent(value).replace(
/%(2[346BF]|3[AC-F]|40|5[BDE]|60|7[BCD])/g,
decodeURIComponent
)
}
}
6.调试验证
6.1 打包编译代码
运行npm run dist命令打包编译代码:
报错,windows系统不认识rm -rf命令, 含义就是如果有dist目录则先删除了,然后重新打包生成dist目录
解决办法 :删除前面的 rm -rf dist/* && 只保留 rollup -c, 重新运行打包命令,可以打包成功
看一下rollup.config.js:
import { terser } from 'rollup-plugin-terser'
import filesize from 'rollup-plugin-filesize'
import license from 'rollup-plugin-license'
import pkg from './package.json'
const licenseBanner = license({
banner: {
content: '/*! <%= pkg.name %> v<%= pkg.version %> | <%= pkg.license %> */',
commentStyle: 'none'
}
})
export default [
{
input: 'src/api.mjs',
output: [
// config for <script type="module">
{
file: pkg.module,
format: 'esm'
},
// config for <script nomodule>
{
file: pkg.browser,
format: 'umd',
name: 'Cookies',
noConflict: true,
banner: ';'
}
],
plugins: [licenseBanner]
},
{
input: 'src/api.mjs',
output: [
// config for <script type="module">
{
file: pkg.module.replace('.mjs', '.min.mjs'),
format: 'esm'
},
// config for <script nomodule>
{
file: pkg.browser.replace('.js', '.min.js'),
format: 'umd',
name: 'Cookies',
noConflict: true
}
],
plugins: [
terser(),
licenseBanner, // must be applied after terser, otherwise it's being stripped away...
filesize()
]
}
]
引入插件:
1.rollup-plugin-filesize :用于显示cli中的文件大小
2.rollup-plugin-terser: 压缩js代码,包括es6代码压缩
3.rollup-plugin-license:将许可证横幅添加到最终捆绑包并输出第三方许可证
引入package.json文件
import pkg from './package.json'
pkg.module: "module": "dist/js.cookie.mjs"
pkg.browser: "browser": "dist/js.cookie.js"
导出的配置中分别定义了打包时不压缩和压缩的两种配置。
6.2 测试
有了上一步打包好的文件之后可以测试一下,源码中的测试逻辑我不是很懂,我是新建了一个文件夹,把打包好的js.cookie.js文件copy过来,有写了一个测试文件,内容如下:
console.log('Cookies',Cookies)
Cookies.set('foo', 'bar')
Cookies.get('foo')
Cookies.set('name', 'value', { expires: 7, path: '' })
Cookies.get("name")
然后把测试文件和js.cookie.js都引入到一个html文件中,浏览器运行这个html文件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="js.cookie.js"></script>
<script src="test.js"></script>
</head>
<body>
</body>
</html>
运行效果如下:
7.疑问以及收获和总结
7.1 疑问
set方法中:
return (document.cookie =
name + '=' + converter.write(value, name) + stringifiedAttributes)
这是不是每一个都重新给document.cookie赋值呢,那么原来写进去的值是不是没了啊?按照我那种方法调试,在调get的时候也没有获取到值,这块不是很清楚耶~
7.2 收获
1.了解到js-cookie的实现原理
2.简单地了解rollup的基础知识,按照rollup中文文档中的教程章节创建了一个项目,学习了一下
3.了解 依赖包 release-it 的用途
学完本期源码,您可以思考如下问题:
1.前端缓存技术都有哪些?
2.为什么要使用cookie, cookie有什么作用和优缺点?
3.cookie和session有什么区别和联系?
4.什么是localStorage?和sessionStorage之间有什么区别?
5.为什么需要对浏览器提供的cookie做进一步的封装?
6.js-cookie有什么特点?
7.谷歌已宣布逐步淘汰三方Cookie,对于使用第三方Cookie的项目该如何兼容?