课程名称:** 破解JavaScript高级玩法
课程章节: 玩转客户端存储
主讲老师: Cloud
今天学习的内容包括:
cookie高级使用和注意事项、indexedDB的使用
客户端写cookie
<!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> </head> <body> <button id="getCookie">获取cookie</button> <button id="setCookie">设置cookie</button> <button id="deleteCookie">删除一个cookie</button> <div class="show-cookie"></div> <script> const domNode = document.querySelector(".show-cookie"); getCookie.onclick = function () { //读 domNode.innerText = document.cookie; }; setCookie.onclick = function () { //写 document.cookie = "testUid=test;path=/;"; }; deleteCookie.onclick = function () { const expiresTime = new Date(0).toUTCString(); //修改cookie或者删除cookie,需要保障path和domain两个值不变。 document.cookie = `testUid=test;path=/;expires=${expiresTime};`; }; </script> </body> </html>
服务端写cookie
<!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> </head> <body> <div class="show-cookie"></div> <script> var xhrObj = new XMLHttpRequest(); xhrObj.onreadystatechange = function () { if (xhrObj.readyState == 4 && xhrObj.status == 200) { const domNode= document.querySelector(".show-cookie"); domNode.innerText=document.cookie; } }; xhrObj.open("post", "http://127.0.0.1:3000/login", true); xhrObj.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); //设置携带cookie xhrObj.withCredentials = true; xhrObj.send("xhr=1"); </script> </body> </html>
CookieUtils
/** * * 编码 -方便后续替换编解码方法 * @param {any} s * @returns */ function encode(s) { return encodeURIComponent(s); } /** * * 解码-方便后续替换编解码方法 * @param {any} s * @returns */ function decode(s) { return decodeURIComponent(s); } /** * * 获取cookie * @param {any} key * @returns */ function getCookieItem(key) { let result = key ? undefined : {}, cookies = document.cookie ? document.cookie.split("; ") : [], i = 0, l = cookies.length; for (; i < l; i++) { let parts = cookies[i].split("="), //取第一个等号前面的作为key name = decode(parts.shift()), cookie = parts.join("="); if (key === name) { result = decode(cookie); break; } if (!key && cookie !== undefined) { //key 未定义,返回全部的key和value对象 result[name] = decode(cookie); } } return result; } /** * * 设置cookie * @param {any} key * @param {any} value * @param {any} [options={}] * @returns */ function setCookieItem(key, value, options={}) { if(!key) return false; console.log(options) let sExpires = ""; if (options.expires) { switch (options.expires.constructor) { case Number: sExpires = options.expires === Infinity ? "; expires=Fri, 31 Dec 9999 23:59:59 GMT" : "; max-age=" + options.expires; break; case String: sExpires = "; expires=" + options.expires; break; case Date: sExpires = "; expires=" + options.expires.toUTCString(); break; } } window.document.cookie = [ encode(key), "=", encode(value), sExpires, options.path ? "; path=" + options.path : "", options.domain ? "; domain=" + options.domain : "", options.secure ? "; secure" : "" ].join(""); return true; } /** * * 移除单个cookie字段 * @param {any} key * @param {any} options * @returns */ function removeCookieItem(key, options) { setCookieItem(key, "", { ...options, expires: -1 }); return !getCookieItem(key); }
对象模型
<!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> <style> * { font-size: 28px; } </style> </head> <body> <div> <button id="btnAdd">添加数据</button> <button id="btnQuery">查询数据</button> </div> <script> // https://github.com/mdn/indexeddb-examples/blob/master/idbkeyrange/scripts/main.js var db; var things = [ { fThing: "Drum kit", fRating: 10 }, { fThing: "Family", fRating: 10 }, { fThing: "Batman", fRating: 9 }, { fThing: "Brass eye", fRating: 9 }, { fThing: "The web", fRating: 9 }, { fThing: "Mozilla", fRating: 9 }, { fThing: "Firefox OS", fRating: 9 }, { fThing: "Curry", fRating: 9 }, { fThing: "Paneer cheese", fRating: 8 }, { fThing: "Mexican food", fRating: 8 }, { fThing: "Chocolate", fRating: 7 }, { fThing: "Heavy metal", fRating: 10 }, { fThing: "Monty Python", fRating: 8 }, { fThing: "Aphex Twin", fRating: 8 }, { fThing: "Gaming", fRating: 7 }, { fThing: "Frank Zappa", fRating: 9 }, { fThing: "Open minds", fRating: 10 }, { fThing: "Hugs", fRating: 9 }, { fThing: "Ale", fRating: 9 }, { fThing: "Christmas", fRating: 8 }, ]; // 插入数据 function insertData() { // 事务 var transaction = db.transaction(["fThings"], "readwrite"); // 对象库 var objectStore = transaction.objectStore("fThings"); // 添加数据 for (i = 0; i < things.length; i++) { var request = objectStore.put(things[i]); } // 成功的回调 transaction.oncomplete = function () { console.log("insert data success"); }; } // 我们先打开一个数据库, window.indexedDB(IDBFactory) const openRequest = window.indexedDB.open("fThings", 1); // 升级的时候创建对象库和对应的索引 openRequest.onupgradeneeded = function (event) { var db = event.target.result; db.onerror = function (event) { console.log("Error loading database"); }; var objectStore = db.createObjectStore("fThings", { keyPath: "fThing", }); objectStore.createIndex("fRating", "fRating", { unique: false }); }; openRequest.onerror = function (event) { console.log("open error:", event); } openRequest.onsuccess = function (event) { console.log("open success"); // 获得database db = event.target.result; }; btnQuery.onclick = function () { // 事务 const transaction = db.transaction(["fThings"], "readonly"); // 对象库 const objectStore = transaction.objectStore("fThings"); // 键 const keyRangeValue = IDBKeyRange.bound("A", "F"); // 游标 objectStore.openCursor(keyRangeValue).onsuccess = function (event) { var cursor = event.target.result; if (cursor) { console.log("value:", cursor.value); cursor.continue(); } else { console.log("Entries all displayed."); } }; }; btnAdd.onclick = insertData; </script> </body> </html>
FileSystem
(function () { const FILE_ERROR = { INITIALIZE_FAILED: '文件系统初始化失败', FILE_EXISTED: '文件已存在', Directory_EXISTED: '目录已存在', ONLY_FILE_WRITE: '只有文件才能写入', NOT_ENTRY: '不是有效的Entry对象', INVALID_PATH: '文件或者目录不能包含\\/:*?"<>|' } const DIR_SEPARATOR = '/' const DIR_OPEN_BOUND = String.fromCharCode(DIR_SEPARATOR.charCodeAt(0) + 1) /** * https://segmentfault.com/q/1010000007499416 * Promise for forEach * @param {*数组} arr * @param {*回调} cb(val)返回的应该是Promise * @param {*是否需要执行结果集} needResults */ const promiseForEach = function promiseForEach(arr, cb, needResults) { const realResult = [] let result = Promise.resolve() Array.from(arr).forEach((val, index) => { result = result.then(() => { return cb(val, index).then((res) => { needResults && realResult.push(res) }) }) }) return needResults ? result.then(() => realResult) : result } const URLUtil = { _pathBlackList: /[\\:*?"<>|]/, // from https://github.com/ebidel/idb.filesystem.js/blob/master/src/idb.filesystem.js // When saving an entry, the fullPath should always lead with a slash and never // end with one (e.g. a directory). Also, resolve '.' and '..' to an absolute // one. This method ensures path is legit! resolveToFullPath(cwdFullPath, path) { let fullPath = path const relativePath = path[0] != DIR_SEPARATOR if (relativePath) { fullPath = cwdFullPath + DIR_SEPARATOR + path } // Normalize '.'s, '..'s and '//'s. const parts = fullPath.split(DIR_SEPARATOR) const finalParts = [] for (let i = 0; i < parts.length; ++i) { const part = parts[i] if (part === '..') { // Go up one level. if (!finalParts.length) { throw Error('Invalid path') } finalParts.pop() } else if (part === '.') { // Skip over the current directory. } else if (part !== '') { // Eliminate sequences of '/'s as well as possible leading/trailing '/'s. finalParts.push(part) } } fullPath = DIR_SEPARATOR + finalParts.join(DIR_SEPARATOR) // fullPath is guaranteed to be normalized by construction at this point: // '.'s, '..'s, '//'s will never appear in it. return fullPath }, isValidatedPath(path) { return this._pathBlackList.test(path) ? false : true } } class FileError { constructor({ code = 999, message = '未知错误' } = { code: 999, message: '未知错误' }) { this.code = code this.message = message } } class Metadata { constructor(modificationTime, size) { this.modificationTime = modificationTime this.size = size } } class FSFile { constructor(name, size, type, lastModifiedDate, blob) { this.name = name this.size = size this.type = type this.lastModifiedDate = lastModifiedDate this.blob = blob } } const ReaderUtil = { read(blob, method) { return new Promise((resolve, reject) => { const reader = new FileReader() const ps = [].slice.call(arguments, 2) ps.unshift(blob) reader[method].apply(reader, ps) reader.onload = function () { return resolve(reader.result) } reader.onerror = function (err) { return reject(err) } reader.onabort = function () { return reject(new Error('读取被中断')) } }) }, readAsArrayBuffer(blob) { return this.read(blob, 'readAsArrayBuffer') }, readAsBinaryString(blob) { return this.read(blob, 'readAsBinaryString') }, readAsDataURL(blob) { return this.read(blob, 'readAsDataURL') }, readAsText(blob, encoding = 'gb2312') { return this.read(blob, 'readAsText', encoding) } } const NOT_IMPLEMENTED_ERROR = new FileError({ code: 1000, message: '方法未实现' }), NOT_SUPPORTED = new Error('浏览器不支持改功能') const Utils = { contentToBlob(content, type = 'text/plain') { let blob // 不是blob,转为blob if (content instanceof Blob) { blob = content } else if (content instanceof ArrayBuffer) { blob = new Blob([new Uint8Array(content)], { type }) } else if (typeof content === 'string') { blob = new Blob([content], { type: 'text/plain' }) } else { blob = new Blob([content], { type }) } return blob } } class Entry { constructor(isFile = true, isDirectory = false, name, fullPath) { this.isFile = isFile this.isDirectory = isDirectory this.name = name this.fullPath = fullPath this.metadata = { lastModifiedDate: new Date(), size: 0 } } /** * 获取元数据 done */ getMetadata() { return this._dispatch('getMetadata') } moveTo() { throw NOT_IMPLEMENTED_ERROR //this._dispatch('moveTo', [...arguments]) } copyTo() { throw NOT_IMPLEMENTED_ERROR // this._dispatch('copyTo', [...arguments]) } toURL() { return this._dispatch('toURL') } /** * 删除 done */ remove() { return this._dispatch('remove') } /** * 获得父目录 done */ getParent() { return this._dispatch('getParent') } } Entry.prototype._dispatch = function (method, ...args) { return new Promise(resolve => { if (FileSystem._instance) { return resolve(FileSystem._instance._provider[method](this, ...args)) } return FileSystem.getInstance().then(fs => { FileSystem._instance = fs return resolve(FileSystem._instance._provider[method](this, ...args)) }) }) } Entry.copyFrom = function (entry) { const en = entry.isFile ? new FileEntry(entry.name, entry.fullPath, entry.file) : new DirectoryEntry(entry.name, entry.fullPath) en.metadata = entry.metadata return en } class FileEntry extends Entry { constructor(name, fullPath, file) { super(true, false, name, fullPath) this.file = file } /** * FileEntry写入数据 done * @param {Blob|String|BufferArray} content * @param {String} type * @param {Boolean} append */ write(content, type = 'text/plain', append = false) { if (!append) { return this._dispatch('write', content, type, append) } return this.append(content) } append(content) { return this.getBlob().then(function (blob) { return this.write(new Blob([blob, Utils.contentToBlob(content, blob.type)])) }.bind(this)) } getBlob() { return this._dispatch('getBlob') } readAsArrayBuffer() { return this._dispatch('readFile', 'readAsArrayBuffer') } readAsBinaryString() { return this._dispatch('readFile', 'readAsBinaryString') } readAsDataURL() { return this._dispatch('readFile', 'readAsDataURL') } readAsText(encoding = 'utf-8') { return this._dispatch('readFile', 'readAsText', encoding) } } class DirectoryEntry extends Entry { constructor(name, fullPath) { super(false, true, name, fullPath) } /** * 获取文件 done * @param {String} path 路径 * @param {Object} options create:是否创建 , exclusive 排他 */ getFile(path, options = { create: true, exclusive: false }) { if (!URLUtil.isValidatedPath(path)) { return Promise.reject(FILE_ERROR.INVALID_PATH) } return this._dispatch('getFile', path, options) } /** * 获取目录 done * @param {String} path * @param {Object} options */ getDirectory(path, options = { create: true, exclusive: false }) { if (!URLUtil.isValidatedPath(path)) { return Promise.reject(FILE_ERROR.INVALID_PATH) } return this._dispatch('getDirectory', path, options) } /** * 递归删除 done */ remove() { return this._dispatch('removeRecursively') } /** * 获取目录下的目录和文件 */ getEntries() { return this._dispatch('getEntries') } ensureDirectory(path) { return this._dispatch('ensureDirectory', path) } } class FileSystem { constructor() { // 实例 this._instance = null // root this.root = null this._provider = null } static getInstance(dbVersion = 1.0) { if (!FileSystem.isSupported) { throw NOT_SUPPORTED } if (this._instance) { return Promise.resolve(this._instance) } return IndexedDBProvider.getInstance(dbVersion, FileSystem._dbName, FileSystem._storeName).then(provider => { this._instance = new FileSystem() this._instance._provider = provider this._instance.root = new DirectoryEntry('/', '/') delete this._instance._instance return this._instance }) } } FileSystem._dbName = '_fs_db_' FileSystem._storeName = '_fs_store' class IndexedDBStoreFactory { static getInstance(dbVersion = 1.0, dbName, storeName) { return new Promise((resolve, reject) => { const request = self.indexedDB.open(dbName, dbVersion) request.onerror = () => { return reject(null) } request.onsuccess = () => { const db = request.result // 老版本,新版本是onupgradeneeded if (db.setVersion && db.version !== dbVersion) { const setVersion = db.setVersion(dbVersion) setVersion.onsuccess = () => { if (!db.objectStoreNames.contains(storeName)) { db.createObjectStore(storeName) } return resolve(db) } } else { return resolve(db) } return resolve(null) } request.onupgradeneeded = event => { const db = event.target.result if (!db.objectStoreNames.contains(storeName)) { db.createObjectStore(storeName) } } }) } } class IndexedDBProvider { constructor(db, storeName) { this._db = db this._storeName = storeName } static getInstance(dbVersion, dbName, storeName) { return IndexedDBStoreFactory .getInstance(dbVersion, dbName, storeName) .then(db => { return new IndexedDBProvider(db, storeName) }) } get transaction() { return this._db.transaction([this._storeName], IDBTransaction.READ_WRITE || 'readwrite') } _toPromise(method, ...args) { try { let suc if (args.length >= 1 && typeof args[args.length - 1] === 'function') { suc = args[args.length - 1] args = args.slice(0, args.length - 1) } return new Promise((resolve, reject) => { // 获得事务 const trans = this.transaction // 获得请求 const req = trans.objectStore(this._storeName)[method](...args) //游标 if (['openCursor', 'openKeyCursor'].indexOf(method) >= 0 && suc) { req.onsuccess = function (event) { suc(event) } trans.oncomplete = function () { return resolve() } trans.onsuccess = function () { return resolve() } } else { // 如果是onsuccess 就返回,只表示请求成功,当大文件存储的时候,并不是已经写入完毕才返回 //req.onsuccess = event => resolve(event.target.result) trans.oncomplete = function () { return resolve(req.result) } trans.onsuccess = function () { return resolve(req.result) } } // 请求失败 req.onerror = () => reject(req.error) // 事务失败 trans.onerror = () => reject(trans.error) }) } catch (err) { return Promise.reject(err) } } /** * * @param {Entry} entry * @param {写入的内容} content * @param {blob类型} type * @param {是否是append模式} append */ write(entry, content, type = 'text/plain') { this._checkEntry(entry) if (entry.isFile !== true) { throw new FileError({ message: FILE_ERROR.ONLY_FILE_WRITE }) } let data = Utils.contentToBlob(content, type) let file = entry.file if (!file) { // 不存在创建 file = new FSFile(entry.fullPath.split(DIR_SEPARATOR).pop(), data.size, type, new Date(), data) entry.metadata.lastModifiedDate = file.lastModifiedDate entry.metadata.size = data.size entry.file = file } else { //存在更新 file.lastModifiedDate = new Date() file.type = type file.size = data.size file.blob = data entry.metadata.lastModifiedDate = file.lastModifiedDate entry.metadata.size = data.size } return this._toPromise('put', entry, entry.fullPath).then(() => entry) } /** * * @param {Entry} entry * @param {String} path * @param {Object} create 是否创建 exclusive排他 */ getFile(entry, path, { create, exclusive }) { return this.getEntry(...arguments, true) } getDirectory(entry, path, { create, exclusive }) { return this.getEntry(...arguments, false) } remove(entry) { this._checkEntry(entry) return this._toPromise('delete', entry.fullPath).then(() => true) } removeRecursively(entry) { this._checkEntry(entry) const range = IDBKeyRange.bound(entry.fullPath, entry.fullPath + DIR_OPEN_BOUND, false, true) return this._toPromise('delete', range).then(() => true) } /** * 获得元数据 * @param {Entry} entry */ getMetadata(entry) { const f = entry.file || {} return new Metadata(f && f.lastModifiedDate || null, f && f.size || 0) } /** * 获取文件或者目录 * @param {Entry} entry * @param {String} path * @param {String} param2 * @param {Boolean} getFile true获取文件 false 获取目录 */ getEntry(entry, path, { create, exclusive = false }, getFile = true) { this._checkEntry(entry) if (path === DIR_SEPARATOR) { // 如果获取'/'直接返回当前目录 return entry } path = URLUtil.resolveToFullPath(entry.fullPath, path) return this._toPromise('get', path).then(fe => { if (create === true && exclusive === true && fe) { //创建 && 排他 && 存在 throw new FileError({ message: getFile ? FILE_ERROR.FILE_EXISTED : FILE_ERROR.Directory_EXISTED }) } else if (create === true && !fe) { //创建 && 文件不存在 const name = path.split(DIR_SEPARATOR).pop(), newEntry = getFile ? new FileEntry(name, path) : new DirectoryEntry(name, path), fileE = getFile ? new FSFile(name, 0, null, new Date(), null) : null if (getFile) newEntry.file = fileE return this._toPromise('put', newEntry, newEntry.fullPath).then(() => { return Entry.copyFrom(newEntry) }) } else if (!create && !fe) { // 不创建 && 文件不存在 return null } else if (fe && fe.isDirectory && getFile || fe && fe.isFile && !getFile) { // 不创建 && entry存在 && 是目录 && 获取文件 || 不创建 && entry存在 && 是文件 && 获取目录 throw new FileError({ code: 1001, message: getFile ? FILE_ERROR.Directory_EXISTED : FILE_ERROR.FILE_EXISTED }) } else { return Entry.copyFrom(fe) } }) } /** * 获得父目录 * @param {Entry} entry */ getParent(entry) { this._checkEntry(entry) // 已经是根目录 if (entry.fullPath === DIR_SEPARATOR) { return entry } const parentFullPath = entry.fullPath.substring(0, entry.fullPath.lastIndexOf(DIR_SEPARATOR)) //上级目录为根目录的情况 if (parentFullPath === '') { return this.root } return this.getDirectory(this.root, parentFullPath, { create: false }, false) } /** * 获得目录下的目录和文件 * @param {Entry} entry */ getEntries(entry) { let range = null, results = [] if (entry.fullPath != DIR_SEPARATOR && entry.fullPath != '') { range = IDBKeyRange.bound( entry.fullPath + DIR_SEPARATOR, entry.fullPath + DIR_OPEN_BOUND, false, true) } let valPartsLen, fullPathPartsLen return this._toPromise('openCursor', range, function (event) { const cursor = event.target.result if (cursor) { const val = cursor.value valPartsLen = val.fullPath.split(DIR_SEPARATOR).length fullPathPartsLen = entry.fullPath.split(DIR_SEPARATOR).length if (val.fullPath !== DIR_SEPARATOR) { // 区分根目录和非根目录 if (entry.fullPath === DIR_SEPARATOR && valPartsLen < fullPathPartsLen + 1 || entry.fullPath !== DIR_SEPARATOR && valPartsLen === fullPathPartsLen + 1) { results.push(val.isFile ? new FileEntry(val.name, val.fullPath, val.file) : new DirectoryEntry(val.name, val.fullPath)) } } cursor['continue']() } }).then(() => results) } toURL(entry) { this._checkEntry(entry) if (entry.file && entry.file.blob) { return URL.createObjectURL(entry.file.blob) } return null } readFile(entry, method, ...args) { this._checkEntry(entry) if (entry.file && entry.file.blob) { return ReaderUtil.read(entry.file.blob, method, ...args) } return null } getBlob(entry) { this._checkEntry(entry) if (entry.file && entry.file.blob) { return entry.file.blob } return null } /** * 检查Entry * @param {*Entry} entry */ _checkEntry(entry) { if (!entry || !(entry instanceof Entry)) { throw new FileError({ message: FILE_ERROR.NOT_ENTRY }) } } /** * * @param {Entry} entry * @param {path} path */ ensureDirectory(entry, path) { this._checkEntry(entry) if (path === DIR_SEPARATOR) { // 如果获取'/'直接返回当前目录 return entry } const rPath = URLUtil.resolveToFullPath(entry.fullPath, path) if (rPath.length < path.length) { return entry } path = rPath.substring(entry.fullPath.length) const dirs = path.split(DIR_SEPARATOR) return promiseForEach(dirs, (dir, index) => { return entry.getDirectory(dirs.slice(0, index + 1).join('/'), { create: true }) }, true).then((dirEntes) => { return dirEntes && dirEntes[dirEntes.length - 1] }).catch(err => { throw err }) } } FileSystem.isSupported = () => { self.indexedDB_ = self.indexedDB || self.mozIndexedDB || self.webkitIndexedDB || self.msIndexedDB self.IDBTransaction = self.IDBTransaction || self.webkitIDBTransaction || self.msIDBTransaction self.IDBKeyRange = self.IDBKeyRange || self.webkitIDBKeyRange || self.msIDBKeyRange return !!(self.indexedDB && self.IDBTransaction && self.IDBKeyRange) } self.FILE_ERROR = FILE_ERROR self.URLUtil = URLUtil self.ReaderUtil = ReaderUtil self.Entry = Entry self.FileEntry = FileEntry self.DirectoryEntry = DirectoryEntry self.FileSystem = FileSystem })(self)