首页
Search
1
Linux 下 Bash 脚本 bad interpreter 报错的解决方法
71 阅读
2
Arch Linux 下解决 KDE Plasma Discover 的 Unable to load applications 错误
52 阅读
3
Arch Linux 下解决 KDE Plasma Discover 的 Unable to load applications 错误
42 阅读
4
如何在 Clash for Windows 上配置服务
40 阅读
5
如何在 IOS Shadowrocket 上配置服务
40 阅读
clash
服务器
javascript
全部
游戏资讯
登录
Search
加速器之家
累计撰写
1,061
篇文章
累计收到
0
条评论
首页
栏目
clash
服务器
javascript
全部
游戏资讯
页面
搜索到
1061
篇与
的结果
2024-10-20
http-proxy-middleware 的注意事项
这篇文章不是 http-proxy-middleware 的使用教程,关于如何使用,还请参考官方教程。主要是说下自己之前使用时的注意事项。 1. 无法继续流转路由 # 如我们在使用 express 等框架时,会声明多个路由的,有的路由是用来处理业务逻辑,有的是收集相关信息,有的是用作拦截器等。import { createProxyMiddleware } from 'http-proxy-middleware'; import express from 'express'; const app = express(); app.use('*', createProxyMiddleware()); app.use('*', () => { console.log('after createProxyMiddleware'); // 无法输出 }); const port = Number(process.env.PORT) || 3001; const ip = process.env.IP || '127.0.0.1'; app.listen(port, ip, () => { const msg = `[server]: Server is running at http://${ip}:${port}`; console.log(msg); }); 若上面 ↑ 的代码,在对*进行拦截请求后,后续的路由服务将不再执行。因此对该路由的一些处理,应当放在 http-proxy-middleware 的前面。我们从源码 ↓ 中就可以看到,createProxyMiddleware()返回的是一个 middleware 方法,即对路由的处理方法:// https://github.com/chimurai/http-proxy-middleware/blob/35ac1dbd29ff0953f978373dd6add081819087de/src/index.ts#L4 export function createProxyMiddleware(options: Options): RequestHandler { const { middleware } = new HttpProxyMiddleware(options); return middleware; } 而在 middlware 的方法 ↓ 中可以看到,若代理过程是正常的,则不会再执行next()的逻辑。// https://github.com/chimurai/http-proxy-middleware/blob/35ac1dbd29ff0953f978373dd6add081819087de/src/http-proxy-middleware.ts#L40 export class HttpProxyMiddleware { public middleware: RequestHandler = async (req, res, next?) => { if (this.shouldProxy(this.proxyOptions.pathFilter, req)) { try { const activeProxyOptions = await this.prepareProxyRequest(req); debug(`proxy request to target: %O`, activeProxyOptions.target); this.proxy.web(req, res, activeProxyOptions); } catch (err) { next && next(err); } } else { next && next(); } } } 因此,后续的路由不会再接收请求。 2. onProxyRes 无法处理 content-length 为 0 的数据 # 我们可以在 onProxyRes 中对获取到的数据进行二次处理,然后再返回。如官方给到的样例intercept-and-manipulate-responses,将 Hello 替换为 GoodBye:const { createProxyMiddleware, responseInterceptor } = require('http-proxy-middleware'); const proxy = createProxyMiddleware({ /** * IMPORTANT: avoid res.end being called automatically **/ selfHandleResponse: true, // res.end() will be called internally by responseInterceptor() /** * Intercept response and replace 'Hello' with 'Goodbye' **/ on: { proxyRes: responseInterceptor(async (responseBuffer, proxyRes, req, res) => { const response = responseBuffer.toString('utf8'); // convert buffer to string return response.replace('Hello', 'Goodbye'); // manipulate response and return the result }), }, }); 但是,若代理转发地址返回的数据,response header 中,不存在content-length字段,或者该字段的值为 0。则 onProxyRes 中的 responseBuffer 为空,无法处理处理数据。如一些 CDN 或者 COS 上的数据,他为了快速响应,或者使用了 chunked 编码(transfer-encoding: chunked),在返回数据时,通常没有 content-length 这个字段。
2024年10月20日
3 阅读
0 评论
0 点赞
2024-10-20
各种 2 到 62 任意进制之间的转换
我们在平时生活中通常会遇到 10 进制转其他进制,或其他进制转为 10 进制,那我们就可以通过 10 进制进行中转,实现各种任意进制的转换。大部分编程语言都仅限在 2-36 进制内的转换,这里我们拓展到 62 进制,即小写字母z的下一位用大写字母A表示,直到大写字母Z。我们先来熟悉下 10 进制之间的转换,然后进行统一。 1. 其他进制转为 10 进制 # 其他进制转为 10 进制时,将当前的基数乘以当前的位权,然后全部相加即可,如一个 8 进制的数字 123:123 = 1 * 8^2 + 2 * 8^1 + 3 * 8^0; = ((1 * 8) + 2) * 8 + 3; 通过这个简单的推导,我们在用代码实现时,可以从左到右,先用当前数字乘以进制,然后再参与下一位的运算,直到结束。进制超过 10 时,会出现字母,这里我们需要把字母转为数字再进行计算。字母转为数字:/** * 将letter转为纯数字 * @param {char} letter * @return {int} */ int transformCharToNum(char letter){ if (letter >= '0' && letter = 'a' && letter = 'A' && letter a num /= to; } reverse(result.begin(), result.end()); return result; } 使用方式:covert10ToOther(13, 2); // 将10进制里的13转为2进制,结果为1101 covert10ToOther(7, 2); // 111 covert10ToOther(13, 5); // 23 3. 任意进制转任意进制 # 任意进制之间的转换,我们只需要把上面的两种方式组合在一起就行了。这里中间需要 10 进制中转一下。我们将进制转换扩展到了 62 进制,里面可能会包含英文字符,因此我们的输入和输出都定义成了 string 类型。若您需要的是纯数字格式的,还请自行转换。class Solution { public: /** * 将num从base进制转为to指定的进制 * @param {string} num 要转换的数字字符串 * @param {int} base num的进制 * @param {int} to 转换后的进制 * @return {string} */ string covert(string num, int base, int to){ // 当base和to相等 或 base和to超出转换范围,则原样返回 if (base == to || !this->checkRadixLegal(base) || !this->checkRadixLegal(to)) { return num; } // 先转成10进制 int p = 0, number10 = 0; while (p < num.length()) { number10 *= base; number10 += this->transformCharToNum(num[p]); p++; } // 若要转换的正好是进制,则直接返回 if (to == 10) { return to_string(number10); } int cur; string result; while (number10) { cur = number10 % to; result.push_back(this->transformNumToChar(cur)); number10 /= to; } reverse(result.begin(), result.end()); return result; } private: bool checkRadixLegal(int radix){ return radix >= 2 && radix = '0' && letter = 'a' && letter = 'A' && letter
2024年10月20日
6 阅读
0 评论
0 点赞
2024-10-20
深入浅出 http 的缓存机制
通过复用以前获取的资源,可以显著提高网站和应用程序的性能。Web 缓存减少了等待时间和网络流量,因此减少了显示资源表示形式所需的时间。通过使用 HTTP 缓存,变得更加响应性。通常 http 缓存分为强缓存和协商缓存。 1. 强缓存 # 在约定的固定时间内,直接使用浏览器本地缓存。这种方式适用于不常更新的资源中,如 js,css,image 等静态资源;同样这也是一个缺点,若缓存时间设置的过长,更新内容不及时;若缓存时间设置的过短,又造成内容没更新,但缓存时间到了的问题。设置强缓存一般有两种方式: 通过 Expires 设置当前资源的过期时间点; 通过 cache-control 设置缓存的时间,从第 1 次请求开始算起; 1.1 Expires # 在服务端设置这个字段,表示该资源的过期时间。Expires: Wed, 21 Oct 2017 07:28:00 GMT 浏览器就会拿其本地时间来跟这个字段中的时间进行对比,若还没到过期时间,则继续使用缓存,否则产生新的请求。这就会存在一个问题,该资源缓存的长短,与其本地时间有关系,比如设置本地电脑时间在 2039 年,目前所有下发的资源,都无法缓存。 1.2 cache-control # Expires 是 HTTP1.0 时就存在字段,为了解决上面 Expires 存在的问题,在 HTTP1.1 中,引入了cache-control字段。该字段不是用统一的过期时间来控制,而是告诉浏览器要缓存多长时间,从第一次收到这个请求开始算起。若我们用 nodejs 搭建一个简单的 http 服务的话,设置该字段也很简单。const http = require('http'); http .createServer((req, res) => { console.log('get static request:', req.url); res.writeHead(200, { 'cache-control': 'public, max-age=31536000', }); res.end('console.log(Date.now())'); }) .listen(3031); 若使用express的话,可以这样设置:const express = require('express'); const app = express(); app.get('*', (req, res) => { console.log('get static request:', req.url); // 下面的3种方式都可以 res.setHeader('cache-control', 'public, max-age=600'); // res.header('cache-control', 'public, max-age=600'); // res.set({ 'cache-control': 'public, max-age=600' }); res.end('console.log(Date.now())'); }); app.listen(3031); 当缓存起作用时,会提示该资源from memory cache或from disk cache,服务端也不再接收该请求。在 Chrome 浏览器中,刷新页面时,会发现 cache-control 或者 Expires 失效了,这是因为: 如果在同一标签中对同一 URI 的另一个请求后立即发出请求(通过单击刷新按钮,或 F5 之类的),Google Chrome 会忽略该标头 Cache-Control 或 Expires 标头。它可能有一个算法来猜测用户真正想做什么。 测试 Cache-Control 标题的一种方法是返回带有自身链接的 HTML 文档。点击该链接后,Chrome 会从缓存中投放文档。 那怎么才能看到效果呢?新开启一个窗口,打开控制台,然后再请求链接,会发现走缓存了,Is Chrome ignoring Cache-Control: max-age?。 1.3 cache-control 的其他用法 # 1.3.1 cache-control 的组成部分 # cache-control 的值主要由 3 部分组成,可以单独或组合使用任何部分,(具体可参考该文档Cache-Control): 可缓存性:即对该资源如何进行缓存 public: 表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存,即使是通常不可缓存的内容。(例如:1.该响应没有 max-age 指令或 Expires 消息头;2. 该响应对应的请求方法是 POST 。); private: 表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它)。私有缓存可以缓存响应内容,比如:对应用户的本地浏览器。 no-cache: 在发布缓存副本之前,强制要求缓存把请求提交给原始服务器进行验证(协商缓存验证)。 no-store: 缓存不应存储有关客户端请求或服务器响应的任何内容,即不使用任何缓存。该no-store比no-cache更加严格,no-cache 是不走强缓存,但协商缓存还可以使用,但 no-store 是不缓存任何内容。 到期时间 max-page=: 设置缓存存储的最大周期,超过这个时间缓存被认为过期(单位秒)。与 Expires 相反,时间是相对于请求的时间; s-maxage=: 覆盖 max-age 或者 Expires 头,但是仅适用于共享缓存(比如各个代理),私有缓存会忽略它; 重新验证和重新加载 must-revalidate: 一旦资源过期(比如已经超过 max-age),在成功向原始服务器验证之前,缓存不能用该资源响应后续请求。 proxy-revalidate: 与 must-revalidate 作用相同,但它仅适用于共享缓存(例如代理),并被私有缓存忽略。 immutable Experimental: 表示响应正文不会随时间而改变。资源(如果未过期)在服务器上不发生改变,因此客户端不应发送重新验证请求头(例如 If-None-Match 或 If-Modified-Since)来检查更新,即使用户显式地刷新页面。在 Firefox 中,immutable 只能被用在 https:// transactions。 1.3.2 使用示例 # 这几个值可以任意的组合,比如:一般不太改变的文件,各个环节都可以缓存,缓存时间为 600s(↓):cache-control: public, max-age=600; 关闭缓存,任何地方都不存储(↓):cache-control: no-store; 客户端可以缓存资源,但每次都必须重新验证其是否有效(↓),如指定no-cache或max-age=0, must-revalidate等:Cache-Control: no-cache; Cache-Control: max-age=0, must-revalidate; 1.4 cache-control 与 Expires 的优先级 # 这两个字段都决定了资源的过期时间,那如果两个同时出现,浏览器要看哪一个呢?根据RFC2616的定义: If a response includes both an Expires header and a max-age directive, the max-age directive overrides the Expires header, even if the Expires header is more restrictive. This rule allows an origin server to provide, for a given response, a longer expiration time to an HTTP/1.1 (or later) cache than to an HTTP/1.0 cache. This might be useful if certain HTTP/1.0 caches improperly calculate ages or expiration times, perhaps due to desynchronized clocks. 当两者同时存在时,max-age 的优先级更高。 1.5 memory cache 和 disk cache 的区别 # Memory Cache 也就是内存中的缓存,主要包含的是当前中页面中已经抓取到的资源,例如页面上已经下载的样式、脚本、图片等。读取内存中的数据肯定比磁盘快,内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。 一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。那么既然内存缓存这么高效,我们是不是能让数据都存放在内存中呢?这是不可能的。计算机中的内存一定比硬盘容量小得多,操作系统需要精打细算内存的使用,所以能让我们使用的内存必然不多。当我们访问过页面以后,再次刷新页面,可以发现很多数据都来自于内存缓存。内存缓存中有一块重要的缓存资源是 preloader 相关指令(例如)下载的资源。总所周知 preloader 的相关指令已经是页面优化的常见手段之一,它可以一边解析 js/css 文件,一边网络请求下一个资源。需要注意的事情是,内存缓存在缓存资源时并不关心返回资源的 HTTP 缓存头 Cache-Control 是什么值,同时资源的匹配也并非仅仅是对 URL 做匹配,还可能会对 Content-Type,CORS 等其他特征做校验。Disk Cache 也就是存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之 Memory Cache 胜在容量和存储时效性上。在所有浏览器缓存中,Disk Cache 覆盖面基本是最大的。它会根据 HTTP Herder 中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。并且即使在跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据。绝大部分的缓存都来自 Disk Cache,关于 HTTP 的协议头中的缓存字段,我们会在下文进行详细介绍。浏览器会把哪些文件丢进内存中?哪些丢进硬盘中?关于这点,网上说法不一,不过以下观点比较靠得住: 对于大文件来说,大概率是不存储在内存中的,反之优先; 当前系统内存使用率高的话,文件优先存储进硬盘; 强缓存下请求数据的流程: 2. 协商缓存 # 根据内容最后的修改时间(Last-Modified),或者标识(ETag),来判断内容是否发生了变化,若没有变化,则告诉浏览器直接使用缓存即可,否则返回最新的内容。协商缓存是每次都要请求服务器的,然后服务器校验是走缓存,还是下发新的内容。当可以使用客户端的缓存时,只需要返回304状态码即可。 2.1 last-modified # last-modified是一个响应首部,表示资源最后一次修改的时间,用来判断接收到的或者存储的资源是否彼此一致。 2.1.1 使用 last-modified 实现缓存 # 若上次响应带有last-modified字段,则后面请求资源时,会自动在头部带上If-Modified-Since字段,值就是上次 last-modified 返回的值。我们服务器就可以通过这个If-Modified-Since字段与资源的修改时间进行对比,若没有变化,直接返回304即可,若产生变化,则下发新的内容和新的last-modified字段。我们以 express 框架为例,来实现下这个缓存逻辑。const express = require('express'); const fs = require('fs'); const app = express(); let lastModifiedTime = 0; app.get('*', (req, res) => { console.log('get static request:', req.url); // 获取请求头中的 if-modified-since ,若有该字段,且能与上次的修改时间相同 // 则直接返回304 const ifModifiedSince = req.headers['if-modified-since']; if (ifModifiedSince && new Date(ifModifiedSince).valueOf() === lastModifiedTime) { res.status(304).end(); return; } const file = './app/static/hunger.js'; // 读取文件时,实际上应当使用 createReadStream ,我们使用readFile仅是为了方便演示, // 因为使用readFile在高并发时,会产生内存积压的问题 // readFile会将所有的内容都读取到内存中, // 而 createReadStream 则是分片读取,只要读取了内容就会传向下一个管道 fs.readFile(file, { encoding: 'utf8' }, (err, data) => { if (err) { return res.status(500).end('read file error'); } const { mtimeMs } = fs.statSync(file); // 存储该文件最后的修改时间,并将其设置为响应头进行返回 lastModifiedTime = Math.floor(mtimeMs); res.setHeader('last-modified', new Date(lastModifiedTime).toGMTString()); res.end(data); }); }); app.listen(3031); 这样就能通过资源的修改时间,来决定是否使用缓存。 2.1.2 last-modified 的缺陷 # 但用这个字段判断一致性时不太准确,原因如下: last-modified 最多只能精确到秒级,若 1 秒内产生多次修改时,会被认为成没有改动; 若文件存在重复上传,或打开文档然后又保存,都可能会造成时间的修改,但内容实际上并没有变化; 针对这些问题,http 协议中又出现了一个etag的响应字段。 2.2 etag # etag 是对资源内容的标识符,如 hash 值。这可以让缓存更高效,并节省带宽,因为如果内容没有改变,Web 服务器不需要发送完整的响应。而如果内容发生了变化,使用 ETag 有助于防止资源的同时更新相互覆盖(“空中碰撞”)。如果给定 URL 中的资源内容更改,则一定要生成新的 Etag 值。 因此 Etags 类似于指纹,也可能被某些服务器用于跟踪。 比较 etags 能快速确定此资源是否变化,但也可能被跟踪服务器永久存留。 2.2.1 使用 etag 实现缓存 # 若上次响应带有etag字段,则后面请求资源时,会自动在头部带上If-None-Match字段,值就是上次 etag 返回的值。我们服务器就可以通过这个If-None-Match字段与资源的 tag 进行对比,若没有变化,直接返回304即可,若产生变化,则下发新的内容和新的etag字段。我们以 express 框架为例,来实现下这个缓存逻辑。const express = require('express'); const fs = require('fs'); const crypto = require('crypto'); const md5 = (str) => crypto.createHash('md5').update(str).digest('hex'); const app = express(); let etag = ''; app.get('*', (req, res) => { console.log('get static request:', req.url); // 获取请求头中的 if-none-match ,若有该字段,且能与上次的etag相同 // 则直接返回304 const ifNoneMatch = req.headers['if-none-match']; if (ifNoneMatch && ifNoneMatch === etag) { res.status(304).end(); return; } const file = './app/static/hunger.js'; // 同理这里实际上我们最好使用 createReadStream fs.readFile(file, { encoding: 'utf8' }, (err, data) => { if (err) { return res.status(500).end('read file error'); } // 这里我们使用md5来生成etag标识 etag = md5(data); res.setHeader('etag', etag); res.end(data); }); }); app.listen(3031); 2.2.2 etag 与 last-modified # 当 etag 与 last-modified 两者同时出现时,etag 的优先级更高;同理,if-none-match 比 if-modified-since 的优先级更高。同时,etag 对资源更新的控制粒度比 last-modified 也更好,无论源文件怎样修改,只要内容不变,etag 就不会变,则缓存就可以一直使用。协商缓存下的请求流程: 3. 总结 # 上面我们讲解了强制缓存与协商缓存的实现机制与实现方式,各位可根据自己资源的缓存特性,来决定使用哪种缓存方式和要缓存的时间。一般地,强制缓存的优先级比协商缓存的优先级更高,优先判断是否存在强制缓存;
2024年10月20日
5 阅读
0 评论
0 点赞
2024-10-20
nodejs 中复制文件和文件夹的多种方式
nodejs 中有多种复制文件的方式,我们一起来看看。 1. copyFile # copyFile()方法的操作最简单,可以直接将文件复制到目标目录中。fs.copyFile('./a.txt', './dist/b.txt'); 异步地将 src 复制到 dest。 默认情况下,如果 dest 已经存在,则会被覆盖。 除了可能的异常之外,没有给回调函数提供任何参数。 Node.js 不保证复制操作的原子性。 如果在打开目标文件进行写入后发生错误,Node.js 将尝试删除目标文件。 但这个方法有一个缺点:目标目录一定要存在(它不会自动创建目录),若不存在时则会抛出异常。因此在使用 copyFile()方法时,一定要确保目录肯定存在,若不存在的话,则需要使用fs.mkdir()或fs.mkdirSync()来创建目录。而且,copyFile()不能复制目录。 2. readFile 和 writeFile # 读取 src 文件的内容,然后再写入到目标文件中。这种方式适合于,在复制过程中,需要修改内容的,再写入目标文件。fs.readFile('./a.txt', { encoding: 'utf8' }, (err, data) => { if (err) { console.error(err); return; } data = data.replace(/hello/gi, 'world'); fs.writeFile('./b.txt', data, (err) => { if (err) { console.error(err); } }); }); 缺点与上面的 copyFile()一样,writeFile()只能在已存在的目录中才能写入文件,readFile()是用来读取文件内容的,因此也无法复制目录。好处就是在复制过程中,可以修改内容。 3. createReadStream 和 createWriteStream # readFile 和 writeFile 是整块的操作数据,若文件比较大,则会系统资源造成压力。而 createReadStream 和 createWriteStream 是采用流的方式来操作数据。fs.createReadStream('./a.txt').pipe(fs.createWriteStream(`./b.txt`)); 4. cp # nodejs 从 16.7.0 版本开始,新加入了一个fs.cp()方法,可以将整个目录结构从 src 异步地复制到 dest,包括子目录和文件。该方法既可以复制某一个文件,也可以复制一个目录。当需要复制目录时,需要将配置中的recursive属性设置为 true。复制文件:// 复制文件 fs.cp('./a.txt', './aa/b.txt', (err) => { if (err) { console.error(err); } }); 复制整个目录,包括子目录:// 复制目录 fs.cp('./aa', './bb', { recursive: true }, (err) => { if (err) { console.error(err); } }); 可以看到,该方法比前面的要好使很多: 不用再确保 dest 目录一定存在,若 dest 目录不存在,则会自动创建(无论几级目录); 可以完整地复制整个文件夹里的文件,包括子目录,不用再递归地单独进行复制; 唯一要做的,就是确认好 nodejs 版本!若您的 nodejs 版本比较低,但又想复制文件夹中的所有文件,怎么办呢?除了可以下一节的 linux 原生 cp 命令,我们还可以用递归的方式来,来复制有的文件:/** * 复制文件夹到目标文件夹 * @param {string} src 源目录 * @param {string} dest 目标目录 * @param {function} callback 回调 */ const copyDir = (src, dest, callback) => { const copy = (copySrc, copyDest) => { fs.readdir(copySrc, (err, list) => { if (err) { callback(err); return; } list.forEach((item) => { const ss = path.resolve(copySrc, item); fs.stat(ss, (err, stat) => { if (err) { callback(err); } else { const curSrc = path.resolve(copySrc, item); const curDest = path.resolve(copyDest, item); if (stat.isFile()) { // 文件,直接复制 fs.createReadStream(curSrc).pipe(fs.createWriteStream(curDest)); } else if (stat.isDirectory()) { // 目录,进行递归 fs.mkdirSync(curDest, { recursive: true }); copy(curSrc, curDest); } } }); }); }); }; fs.access(dest, (err) => { if (err) { // 若目标目录不存在,则创建 fs.mkdirSync(dest, { recursive: true }); } copy(src, dest); }); }; 使用方式:copyDir('./aa', './abc/ddd'); 5. linux 中的 cp 命令 # 我们可以使用 child_process 中的exec或spawn等来执行 linux 中的原生命令。而linux 中的 cp 命令就是用来复制文件或者目录的。const { exec, spawn } = require('child_process'); exec('cp ./aa/a.txt ./bb/b.txt'); // 复制文件时,需要确保目标目录存在 exec('cp -r ./aa ./bb/cc/dd'); // 复制文件夹,目标目录可以自动创建 spawn('cp', ['-r', './aa', './bb/cc/dd']);
2024年10月20日
5 阅读
0 评论
0 点赞
2024-10-20
如何重写 localStorage 中的方法
经常会有同学想要重写 localStorage 中的方法,来实现 key 的过期时间,或者监听 key 的读写等。那么都有哪些方法重写 localStorage 里的方法呢? 1. 直接在 localStorage 上重写 # 很多同学喜欢重写的思路,先留存原生的方法,然后直接在 localStorage 上重写该方法,如:const { setItem } = localStorage; localStorage.setItem = function (key, value) { console.log('localStorage.setItem', key, value); setItem.call(this, key, value); }; 不过这种写法,并不是重写setItem()方法,而是在 localStorage 上添加了一个 setItem 的属性,该属性的值就是后面声明的方法,然后把原生的 setItem() 方法给覆盖掉了。具体我没有太测试,不过在有的浏览器里,会忽略该属性,导致我们的重写失效。 2. 重写 localStorage.__proto__ 上的方法 # 我们仔细观察的话,setItem、getItem 都是以 __proto__ 的方式继承自 Storage 的。那我们直接重写 localStorage.__proto__ 上面的方法。const { setItem } = localStorage.__proto__; localStorage.setItem = function (key, value) { console.log('localStorage.__proto__.setItem', key, value); setItem.call(this, key, value); }; 这就实现了 setItem()方法真正的重写。但这里还有个问题,localStorage 和 sessionStorage 都是继承自Storage,重写了 localStorage.__proto__ 上的属性或方法后,也把 sessionStorage 里的方法重写了。 3. 外部封装一层 # 我们不直接对 localStorage 本身的方法进行修改,而是在外面包装一层,底层再使用 localStorage 实现存储功能。class MyLocalStorage { setItem(key, value) { console.log('MyLocalStorage.setItem', key, value); localStorage.setItem(key, value); } } const myLocalStorage = new MyLocalStorage(); myLocalStorage.setItem('aa', '123'); 这种方式相对来说,自由度会高一些,而且也没有第 1 节中的兼容性问题。只是使用的名称发生了变化,而且还完全把 localStorage 里的属性和方法都屏蔽掉了。若想没有负担地使用自定义的对象,则需要把所有的属性和方法都实现了。无法像上面那种,单独 mock 某个方法。 4. 覆盖 localStorage # 使用Object.defineProperty或Proxy等方式,完全覆盖掉 localStorage 变量。比第 3 节好的地方在于名称没变。 4.1 直接覆盖,没有效果 # 若使用下面的方式直接覆盖的话,其实是没有效果的。window.localStorage = Object.create(null); console.log(window.localStorage); // 还是原生的 我们通过 Object.getOwnPropertyDescriptor 获取 localStorage 的属性描述符。可以发现并没有writable: true的属性,这说明 localStorage 并不是直接可写的。 4.2 使用 Object.defineProperty 重写 # 既然没有writable属性,那我们就给他加一个。我们可以用Object.defineProperty来重写 localStorage。但就不能用上面外面包一层的写法了,若直接将上面的 myLocalStorage 给到 localStorage 的话,会产生无限递归(为避免生成误导,这里就不写错误的写法了)。((win) => { const nativeLocalStorage = win.localStorage; win.nativeLocalStorage = nativeLocalStorage; // 保留原生的使用 class MyLocalStorage { setItem(key, value) { console.log('MyLocalStorage.setItem', key, value); nativeLocalStorage.setItem(key, value); } getItem(key) { console.log('MyLocalStorage.getItem', key); return nativeLocalStorage.getItem(key); } } const myLocalStorage = new MyLocalStorage(); // 将新创建的实例赋值给localStorage Object.defineProperty(win, 'localStorage', { value: myLocalStorage, writable: true, }); })(window); 我这里对 localStoage 进行了下备份,万一要需要原生的方法时,还可以操作一下。 5. 总结 # 这篇文章里,我们并没有具体地实现某个功能,如设置过期时间等,而是从另一个角度讲了下如何重写 localStorage 或者里面的方法。
2024年10月20日
4 阅读
0 评论
0 点赞
1
...
49
50
51
...
213