首页
Search
1
Linux 下 Bash 脚本 bad interpreter 报错的解决方法
70 阅读
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
带你深入领略 Proxy 的世界
Proxy 是 es2015 标准规范加入的语法,很可能你只是听说过,但并没有用过,毕竟考虑到兼容的问题,不能轻易地使用 Proxy 特性。但现在随着各个浏览器的更新迭代,Proxy 的支持度也越来越高:而且使用 Proxy 进行代理和劫持,也渐渐成为了趋势。Vue3 已经用 Proxy 代替了 Object.defineProperty 实现响应式,mobx 也从 5.x 版本开始使用 Proxy 进行代理。 1. Proxy 的基本结构 # Proxy 的基本使用方式:/** * target: 表示要代理的目标,可以是object, array, function类型 * handler: 是一个对象,可以编写各种代理的方法 */ const proxy = new Proxy(target, handler); 例如我们想要代理一个对象,可以通过设置 get 和 set 方法来代理获取和设置数据的操作:const person = { name: 'wenzi', age: 20, }; const personProxy = new Proxy(person, { get(target, key, receiver) { console.log(`get value by ${key}`); return target[key]; }, set(target, key, value) { console.log(`set ${key}, old value ${target[key]} to ${value}`); target[key] = value; }, }); Proxy 仅仅是一个代理,personProxy 上有 person 所有的属性和方法。我们通过personProxy获取和设置 name 时,就会有相应的 log 输出:personProxy.name; // "wenzi" // log: get value by name personProxy.name = 'hello'; // log: set name, old value wenzi to hello 并且通过 personProxy 设置数据时,代理的原结构里的数据也会发生变化。我们打印下 person,可以发现字段 name 的值 也变成了hello:console.log(person); // {name: "hello", age: 20} Proxy 的第 2 个参数 handler 除了可以设置 get 和 set 方法外,还有更多丰富的方法: get(target, propKey, receiver):拦截对象属性的读取,比如 proxy.foo 和 proxy['foo']。 set(target, propKey, value, receiver):拦截对象属性的设置,比如 proxy.foo = v 或 proxy['foo'] = v,返回一个布尔值。 has(target, propKey):拦截 propKey in proxy 的操作,返回一个布尔值。 deleteProperty(target, propKey):拦截 delete proxy[propKey]的操作,返回一个布尔值。 ownKeys(target):拦截 Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in 循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而 Object.keys()的返回结果仅包括目标对象自身的可遍历属性。 getOwnPropertyDescriptor(target, propKey):拦截 Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。 defineProperty(target, propKey, propDesc):拦截 Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。 preventExtensions(target):拦截 Object.preventExtensions(proxy),返回一个布尔值。 getPrototypeOf(target):拦截 Object.getPrototypeOf(proxy),返回一个对象。 isExtensible(target):拦截 Object.isExtensible(proxy),返回一个布尔值。 setPrototypeOf(target, proto):拦截 Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。 apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如 proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。 construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如 new proxy(...args)。 如我们通过 delete 删除其中一个元素时,可以通过deleteProperty()方法来拦截这个操作。还是上面代理 person 的代码,我们添加一个 deleteProperty:const person = { name: 'wenzi', age: 20, }; const personProxy = new Proxy(person, { // 忽略get和set方法,与上面一样 // ... deleteProperty(target, key, receiver) { console.log(`delete key ${key}`); delete target[key]; }, }); 当执行 delete 操作时:delete personProxy['age']; // log: delete key age 2. Proxy 与 Reflect # Proxy 与 Reflect 可以说形影不离了,Reflect 里所有的方法和使用方式与 Proxy 完全一样。例如上面 Proxy 里的 get(), set()和 deleteProperty()方法我们都是直接操作原代理对象的,这里我们改成使用Reflect来操作:const personProxy = new Proxy(person, { get(target, key, receiver) { console.log(`get value by ${key}`); return Reflect.get(target, key, receiver); }, set(target, key, value, receiver) { console.log(`set ${key}, old value ${target[key]} to ${value}`); return Reflect.set(target, key, value, receiver); }, deleteProperty(target, key, receiver) { console.log(`delete key ${key}`); return Reflect.deleteProperty(target, key, receiver); }, }); 可以发现完美地实现这些功能。 3. 代理数组 # 我们在之前的文章 Vue 中对数组特殊的操作 中,讨论过 Vue 为什么没有使用Object.defineProperty来劫持数据,而是重写了 Array 原型链上的几个方法,通过这几个方法来实现 Vue 模板中数据的更新。但若 Proxy 的话,就可以直接代理数组:const arr = [1, 2, 3, 4]; const arrProxy = new Proxy(arr, { get(target, key, receiver) { console.log('arrProxy.get', target, key); return Reflect.get(target, key, receiver); }, set(target, key, value, receiver) { console.log('arrProxy.set', target, key, value); return Reflect.set(target, key, value, receiver); }, deleteProperty(target, key) { console.log('arrProxy.deleteProperty', target, key); return Reflect.deleteProperty(target, key); }, }); 现在我们再来操作一下代理后的数组 arrProxy 看下:arrProxy[2] = 22; // arrProxy.set (4)?[1, 2, 3, 4] 2 22 arrProxy[3]; // arrProxy.get (4)?[1, 2, 22, 4] 3 delete arrProxy[2]; // arrProxy.deleteProperty (4)?[1, 2, 22, 4] 2 arrProxy.push(5); // push操作比较复杂,这里进行了多个get()和set()操作 arrProxy.length; // arrProxy.get (5)?[1, 2, empty, 4, 5] length 可以看到无论获取、删除还是修改数据,都可以感知到。还有数组原型链上的一些方法,如: push() pop() shift() unshift() splice() sort() reverse() 也都能通过 Proxy 中的代理方法劫持到。concat()方法比较特殊的是,他是一个赋值操作,并不改变原数组,因此在调用 concat()方法操作数组时,如果没有赋值操作,那么这里只有 get()拦截到。 4. 代理函数 # Proxy 中还有一个apply()方法,是表示自己作为函数调用时,被拦截的操作。const getSum = (...args) => { if (!args.every((item) => typeof item === 'number')) { throw new TypeError('参数应当均为number类型'); } return args.reduce((sum, item) => sum + item, 0); }; const fnProxy = new Proxy(getSum, { /** * @params {Fuction} target 代理的对象 * @params {any} ctx 执行的上下文 * @params {any} args 参数 */ apply(target, ctx, args) { console.log('ctx', ctx); console.log(`execute fn ${getSum.name}, args: ${args}`); return Reflect.apply(target, ctx, args); }, }); 执行 fnProxy:// 10, ctx为undefined, log: execute fn getSum, args: 1,2,3,4 fnProxy(1, 2, 3, 4); // ctx为undefined, Uncaught TypeError: 参数应当均为number类型 fnProxy(1, 2, 3, '4'); // 10, ctx为window, log: execute fn getSum, args: 1,2,3,4 fnProxy.apply(window, [1, 2, 3, 4]); // 6, ctx为window, log: execute fn getSum, args: 1,2,3 fnProxy.call(window, 1, 2, 3); // 6, ctx为person, log: execute fn getSum, args: 1,2,3 fnProxy.apply(person, [1, 2, 3]); 5. 一些简单的应用场景 # 我们知道 Vue3 里已经用 Proxy 重写了响应式系统,mobx 也已经用了 Proxy 模式。在可见的未来,会有更多的 Proxy 的应用场景,我们这里也稍微讲解几个。 5.1 统计函数被调用的上下文和次数 # 这里我们用 Proxy 来代理函数,然后函数被调用的上下文和次数。const countExecute = (fn) => { let count = 0; return new Proxy(fn, { apply(target, ctx, args) { ++count; console.log('ctx上下文:', ctx); console.log(`${fn.name} 已被调用 ${count} 次`); return Reflect.apply(target, ctx, args); }, }); }; 现在我们来代理下刚才的getSum()方法:const getSum = (...args) => { if (!args.every((item) => typeof item === 'number')) { throw new TypeError('参数应当均为number类型'); } return args.reduce((sum, item) => sum + item, 0); }; const useSum = countExecute(getSum); useSum(1, 2, 3); // getSum 已被调用 1 次 useSum.apply(window, [2, 3, 4]); // getSum 已被调用 2 次 useSum.call(person, 3, 4, 5); // getSum 已被调用 3 次 5.2 实现一个防抖功能 # 基于上面统计函数调用次数的功能,也给我们实现一个函数的防抖功能添加了灵感。const throttleByProxy = (fn, rate) => { let lastTime = 0; return new Proxy(fn, { apply(target, ctx, args) { const now = Date.now(); if (now - lastTime > rate) { lastTime = now; return Reflect.apply(target, ctx, args); } }, }); }; const logTimeStamp = () => console.log(Date.now()); window.addEventListener('scroll', throttleByProxy(logTimeStamp, 300)); logTimeStamp()至少需要 300ms 才能执行一次。 5.3 实现观察者模式 # 我们在这里实现一个最简单类 mobx 观察者模式。const list = new Set(); const observe = (fn) => list.add(fn); const observable = (obj) => { return new Proxy(obj, { set(target, key, value, receiver) { const result = Reflect.set(target, key, value, receiver); list.forEach((observer) => observer()); return result; }, }); }; const person = observable({ name: 'wenzi', age: 20 }); const App = () => { console.log(`App -> name: ${person.name}, age: ${person.age}`); }; observe(App); person就是使用 Proxy 创建出来的代理对象,每当 person 中的属性发生变化时,就会执行 App()函数。这样就实现了一个简单的响应式状态管理。 6. Proxy 与 Object.defineProperty 的对比 # 上面很多例子用Object.defineProperty也都是可以实现的。那么这两者都各有什么优缺点呢? 6.1 Object.defineProperty 的优劣 # Object.defineProperty的兼容性可以说比 Proxy 要好很多,出特别低的 IE6,IE7 浏览器外,其他浏览器都有支持。但 Object.defineProperty 支持的方法很多,并且主要是基于属性进行拦截的。因此在 Vue2 中只能重写 Array 原型链上的方法,来操作数组。 6.2 Proxy 的优劣 # Proxy与上面的正好相反,Proxy 是基于对象来进行代理的,因此可代理更多的类型,例如 Object, Array, Function 等;而且代理的方法也多了很多。劣势就是兼容性不太好,即使用 polyfill,也无法完美的实现。 7. 总结 # Proxy 能实现的功能还有很多,后面我们也会继续进行探索,并且尽可能去了解下基于 Proxy 实现的类库,例如 mobx5 的源码和实现原理等。
2024年10月20日
3 阅读
0 评论
0 点赞
2024-10-20
使用 nodejs 中的 http 模块实现几个超实用的工具
nodejs 方便了我们前端开发者进行一些服务端上的操作,可以进行无缝地衔接。像其他一些后端语言,如 php, golang, java 等,都需要一定的学习成本,而 nodejs 则就是为前端开发者定制的。在 nodejs 中,提供了原生的 http 模块,我们可以利用 http 模块搞几个常用的小工具,能够大大方便我们的工作。我在之前从 0 到 1 学习 node(二)之搭建 http 服务器的文章中也讲解过 http 模块,不过我们这里主要是利用 http 模块来打造几个使用的工具。 1. 一个简单的 HTTP 服务 # 使用 nodejs 搭建 http 服务非常地简单,只需要引入 http 模块即可:// server.js const http = require('http'); const ip = '127.0.0.1'; const port = 3000; http .createServer((req, res) => { res.end('heelo world!'); }) .listen(port, ip); console.log(`server has started at ${ip}:${port}`); 然后执行 server.js 文件:$node server.js 在控制台就会输出:server has started at 127.0.0.1:3000 然后在浏览器上访问http://127.0.0.1:3000,就能看到**hello world!**的输出。如果想在启动时,通过参数指定 IP 或端口。那么我们可以通过process.env来获取命令行中指定的参数:// 通过process.env来获取指定的参数 // 并设置兜底的数据 const ip = process.env.IP || '127.0.0.1'; const port = process.env.PORT || 3000; 在执行 sever.js 时,就可以通过参数指定 IP 和端口:$PORT=3030 node app.js 2. 一个有延迟的请求 # 在做开发和调试过程中,经常需要考虑到一个请求或者图片等加载很慢时,应该怎么处理。比如有用户反馈某些图片加载很慢,导致页面看起来不正常。那么我就应该针对图片加载慢进行一些处理,可是怎么模拟这个加载很慢的图片呢?很多现有的接口,都无法模拟出这种有特殊延迟的情况,这里我们可以使用 http 模块来实现。我们在第 1 个 http 服务器的基础上,进行改进。 res.end()方法会告知服务器当前次响应结束,若没有调用,则一直处于等待状态。 我们要实现一个有延迟的响应,可以使用 setTimeout 延迟调用 end()方法即可。这里先实现一个有延迟的接口。http .createServer((req, res) => { const index = req.url.indexOf('?'); if (index >= 0) { const query = req.url.slice(index); const ss = new URLSearchParams(query); const timeout = ss.get('timeout'); const type = ss.get('type'); if (timeout && Number(timeout)) { return setTimeout(() => { if (type === 'json') { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ code: 0, msg: 'hello world' })); } else if (type === 'image') { // 输出本地一个图片 } else { res.end(`delay ${timeout}ms response`); } }, Number(timeout)); } } res.end('hello world!'); }) .listen(port, ip); 想要延迟输出图片时,需要通过数据流的方式读取本地的图片,然后输出到前端:const stream = fs.createReadStream('./img/s.jpg'); const responseData = []; //存储文件流 if (stream) { //判断状态 stream.on('data', function (chunk) { responseData.push(chunk); }); stream.on('end', function () { const finalData = Buffer.concat(responseData); // response.write(); res.writeHead(200, { 'Content-Type': 'image/jpg' }); res.end(finalData); }); } 3. 实现接口的中转代理 # 我们有时会遇到需要的接口存在跨域,或者是内网接口的问题,这时我们就需要通过一个中间层,来对接口进行中转代理,才能正常地访问接口。 3.1 原生 http 模块来实现 # 实现接口代理时要注意亮点: 透传,接收到的所有数据,根据需要尽量都传给代理的接口,例如 cookie,参数等; 设置跨域头,为了方便前端的访问,我们需要在返回的头部加上 3 个可以跨域的字段; 跨域的方式有很多种,比如 jsonp 也是其中一种,但 cors 跨域是比较好的一种,前端可以有效地控制请求时间和取消请求。在设置跨域头Access-Control-Allow-Origin时,这里是不建议直接设置成*。一方面是不安全,所有的域名都可以访问;再有就是前端不会再传送 cookie,无法进行一些登录态的校验等。在设置Access-Control-Allow-Origin之前,我们要先校验下 headers 中的 referer,如果为空或者不满足白名单的要求,则可以直接返回 403。const allowList = ['joke.qq.com', 'www.qq.com']; if (!req.headers || !req.headers.referer) { res.writeHead(403, 'forbidden'); res.end('403 forbidden'); return; } const { hostname } = new URL(req.headers.referer); if (!allowList.includes(hostname)) { res.writeHead(403, 'forbidden'); res.end('403 forbidden'); return; } 满足要求之后,需要将 referer 最后的斜杠/去掉,否则会设置不成功。完成的代码样例如下:const http = require('http'); const https = require('https'); const ip = process.env.IP || '127.0.0.1'; const port = process.env.PORT || 3001; http .createServer((req, res) => { const allowList = ['joke.qq.com', 'www.qq.com']; if (!req.headers || !req.headers.referer || allow) { res.writeHead(403, 'forbidden'); res.end('403 forbidden'); return; } console.log('发起请求', req.headers); https .get('https://www.v2ex.com/api/topics/latest.json', (response) => { let data = ''; response.on('data', (chunk) => { data += chunk; }); response.on('end', () => { res.setHeader('Access-Control-Allow-Origin', (req.headers.referer || '').replace(/\/$/, '')); res.setHeader('Access-Control-Allow-Methods', 'GET, POST'); res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type'); res.end(data); }); }) .on('error', (e) => { console.error(`请求遇到问题: ${e.message}`, e); res.end('error'); }); }) .listen(port, ip); console.log(`server has started at ${ip}:${port}`); 3.2 proxy 代理组件 # 若需要代理更多的接口,或者路径是从前端传过来的,我们自己倒是也可以实现,不过还有更方便的 proxy 代理组件了。这里我们用 http-proxy 组件来实现:const http = require('http'); const httpProxy = require('http-proxy'); const ip = process.env.IP || '127.0.0.1'; const port = process.env.PORT || 3000; const proxy = httpProxy.createProxyServer({ target: 'https://www.v2ex.com', // 代理的接口地址 changeOrigin: true, }); http .createServer((req, res) => { // 设置跨域头 res.setHeader('Access-Control-Allow-Origin', (req.headers.referer || '').replace(/\/$/, '')); res.setHeader('Access-Control-Allow-Methods', 'GET, POST'); res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type'); // 将请求和响应的对象传给proxy proxy.web(req, res); }) .listen(port, ip); 然后前端直接按照路径发起请求即可:axios('http://localhost:3000/api/topics/latest.json').then(console.log).catch(console.error); 4. 模拟数据 # 前端在写页面逻辑时,经常要考虑到数据的各种情况,比如无数据时,长列表,各种长度的昵称等。无论是读取配置的 json 文件,还是用代码生成的数据,都不具有随机性。现在,我们可以利用 mockjs 来实现各种数据的模拟:const http = require('http'); const Mock = require('mockjs'); const ip = process.env.IP || '127.0.0.1'; const port = process.env.PORT || 3000; http .createServer((req, res) => { const result = Mock.mock({ code: 0, msg: 'success', 'x-from': 'mock', data: Mock.mock({ 'rank|20': [ { 'no|+1': 1, // no 字段从 1 开始自增 uin: () => Mock.Random.string(32), // 32 长度的随机字符串 nick: () => Mock.Random.string(1, 20), // 长度在 1-20 之间的随机字符串 face: () => Mock.Random.image('120x120'), // 120*120 的图片 score: () => Mock.Random.integer(1, 2000), // 分数 }, ], }), }); res.setHeader('Access-Control-Allow-Origin', (req.headers.referer || '').replace(/\/$/, '')); res.setHeader('Access-Control-Allow-Methods', 'GET, POST'); res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type'); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(result, null, 2)); }) .listen(port, ip); 生成的数据:关于 Mockjs 更多的语法规则,可以访问https://github.com/nuysoft/Mock/wiki/Getting-Started。 5. 总结 # 使用 nodejs 还可以实现更多的功能,这里我们也仅仅是实现了其中简单的几个。基于我们的前端知识和业务需求,我们还能实现更多的小工具。
2024年10月20日
5 阅读
0 评论
0 点赞
2024-10-20
公众号改名叫“前端小茶馆”啦
之前公众号的名字一直是叫“蚊子的博客”,后来觉得这个名字没什么特色,也没有有突出业务特点。想了很长时间,终于把公众号的名字改成了“前端小茶馆”。就是希望这里作为一个休憩的地方,慢慢品读文章里的知识。感谢一直还在关注着我的朋友们,我也会持续输出一些多少有点质量的文章。或许只有在孤独中,我们才可以静静地、默默地谛听,谛听这个灯火阑珊的城市,谛听城市之外的旷野、远山,和比远山更远的一切生生不息的律动,同时也谛听自己生命的足音以及深邃的内心。或许在孤独中,我们才能强烈意识到生命荣枯兴衰的不朽轮回,从而坦然应对到来的一切。人生旅途中孤独是必然的,在真正的孤独中我们才能不断地成长。
2024年10月20日
2 阅读
0 评论
0 点赞
2024-10-20
JS 中一些高效的魔法运算符
这两天有同事说我挺久没更新文章了,不过最近一段时间确实有点忙,一直在忙着两个活动中心的改版,没什么时间来更新文章。今天终于抽空可以写一写。JavaScript 现在每年都会发一个新版本,里面也会新增一些操作更加便利、高效的运算符。今天我们就来盘点下几个高效的魔法运算符。 1. 可选链运算符 # 之前当我们想要使用某个结构比较深的属性,同时又无法确定所有的父级一定存在时,我们需要进行一连串的判断,例如一个数据结构:const student = { score: { math: 98, }, }; 我们想要获取最内层的 math 属性的值时:if (student && student.score) { console.log(student.score.math); } 1.1 获取深层次的属性 # 不过当我们使用可选链运算符后,判断就简单很多了,可选链运算符会在链路上遇到 null 或者 undefined 时,直接返回 undefined,而不会抛出错误异常:console.log(student?.score?.math); 1.2 执行一个可选的方法 # 同时在执行一个可能存在的函数时,也可以用到。例如一个 react 组件中,传入的方法是可选的:// getScore 是一个可选参数,要么是 undefined,要么是一个函数 const Student = ({ getScore }: { getScore?: () => void }) => { useEffect(() => { // 当 getScore 存在时,正常执行 getScore()方法 getScore?.(); }, []); return ; }; 或者我们执行一个 dom 元素的方法时,也可以使用。document.querySelector 会返回两种类型,当 dom 元素真实存在时会返回该元素,否则返回 null。写过 typescript 的都知道,当我们要调用某个方法时,总是要先确定该 dom 元素是存在的:const dom = document.querySelector('.score'); if (dom) { dom.getBoundingClientRect(); // 当 dom 元素存在时,才执行该方法 } 使用可选链操作符时,就直接调用即可:document.querySelector('.score')?.getBoundingClientRect(); 1.3 获取数组中的值 # 若数组存在,则获取某个下标的值,我们现在也不用再判断数组是否存在了,可以直接使用:arr?.[1]; // 若 arr 存在时,则正常获取 arr[1]中的值 上面的 3 种情况也是可以组合使用的。若一个结构比较复杂时,各种类型都有,这里我们要执行数组 math 下标 2 的方法:const student = { score: { math: [ 98, 67, () => { return 99; }, ], }, }; 执行:student?.score?.math?.[2]?.(); // 99 1.4 无法进行赋值操作 # 可选链运算符只能执行获取操作,是无法进行赋值操作的。例如给一个可能的数组或者 dom 元素赋值时,会直接抛出语法异常:arr?.[1] = 2; // x document.querySelector('.score')?.innerHTML = 98; // x 当我们执行上面的语句时,会抛出如下的提示: Uncaught SyntaxError: Invalid left-hand side in assignment 即不能给左侧的可选链进行赋值操作。 2. 双问号运算符 # 双问号运算符??,我理解是为了解决或运算符||而设计出来的。我们先来回顾下或运算符的操作,当左侧的数据为假值(数字 0, 布尔类型的 false,空字符串,undefined, null)时,则执行右侧的语句。false || 123; 0 || 123; '' || '123'; undefined || 123; null || 123; 可是在有些情况下,false 和 0 都是正常的值,但若使用或运算符时,会导致出错。比如下面的这个例子,当 score 为空时,则默认值为 1。当输入正常值 0 时应当返回 0(但实际上返回了 1):const getSCore = (score: number) => { return score || 1; }; getScore(0); // 1 这时,我们就用到了双问号运算符??。双问号运算符只会在左侧为 undefined 或者 null 时,才会执行右侧的语句。const getSCore = (score: number) => { return score ?? 1; }; getScore(0); // 0 同时,双问号运算符还可以与=结合成为一个赋值操作,当左侧为 null 或者 undefined 时,则将右侧语句的结果赋值给左侧的变量:score ??= 1; // 1 3. 或运算和与运算的赋值操作 # 我们在之前使用或运算符进行赋值操作时,是这样写的:score = score || 1; age = age && 24; 现在可以直接简写成:score ||= 1; // 等同于 score = score || 1 age &&= 24; // 等同于 age = age && 24 4. 双星号运算符 # 双星号运算符**是比较早引入到 js 中的,只是我们用到的比较少而已。其实它执行的是一个幂运算,等同于 Math.pow()。2 ** 10; // 1024, 2的10次方,等同于 Math.pow(2, 10); 5. 总结 # 以上所有的样例均已在 chrome90 上运行通过。我们在已经有 babel 帮助转换的情况下,可以适当的在代码运用这些运算符,能够极大地简化我们的代码。
2024年10月20日
3 阅读
0 评论
0 点赞
2024-10-20
如何解决移动端的安全区域为0的问题
从 iPhone X 开始出现了刘海和底部的黑条的区域,而 Android 系统通常也会模仿 iPhone 的一些设计,然后就是现在越来越多地新机型有了安全区域的概念。若完全不考虑这些,可能就会出现类似这样的情况:因此我们需要对这些区域做些特殊地处理。 1. 适配 iOS 中的安全区域 # 在大部分机型,尤其是 iOS 设备中,适配安全区域还是比较简单的,主要是 3 个步骤。 1.1 设置网页在可视窗口的布局方式 # 新增 viweport-fit 属性,使得页面内容完全覆盖整个窗口: 只有设置了 viewport-fit=cover,才能使用 env()。 1.2 限定安全区域 # iOS11 新增特性,Webkit 的一个 CSS 函数,用于设定安全区域与边界的距离,有四个预定义的变量: safe-area-inset-left:安全区域距离左边边界的距离 safe-area-inset-right:安全区域距离右边边界的距离 safe-area-inset-top:安全区域距离顶部边界的距离 safe-area-inset-bottom:安全区域距离底部边界的距离 这里我们只需要关注safe-area-inset-bottom这个变量,因为它对应的就是小黑条的高度。注意:当 viewport-fit=contain 时 env() 是不起作用的,必须要配合 viewport-fit=cover 使用。对于不支持 env() 的浏览器,浏览器将会忽略它。 The env() function shipped in iOS 11 with the name constant(). Beginning with Safari Technology Preview 41 and the iOS 11.2 beta, constant() has been removed and replaced with env(). You can use the CSS fallback mechanism to support both versions, if necessary, but should prefer env() going forward. 这就意味着,之前使用的 constant() 在 iOS11.2 之后就不能使用的,但我们还是需要做向后兼容,像这样:body { padding-bottom: constant(safe-area-inset-bottom); /* 兼容 iOS < 11.2 */ padding-bottom: env(safe-area-inset-bottom); /* 兼容 iOS >= 11.2 */ } 注意:env() 跟 constant() 需要同时存在,而且顺序不能换。在竖屏的情况下,若要限定顶部的刘海区域,则要使用safe-area-inset-top;若要限定底部的区域,则要使用safe-area-inset-bottom。若需要进行计算,则可以使用calc()函数:body { padding-bottom: calc(12px + constant(safe-area-inset-bottom)); padding-bottom: calc(12px + env(safe-area-inset-bottom)); } 2. 部分奇特的 Android 手机 # 很多 Android 手机也会按照 iOS 的标准来实现安全区域,因此上面的属性在大部分 Android 手机上也能正常使用。但是,我们在测试的过程中发现,有几个奇特的手机,会出现下图的状况:通过 Chrome 查看样式,发现他会识别 safe-area-inset-top 等预定义变量,但又将其解析为 0。这就导致即使我们设置了兜底的数据,也无法使用。例如我们在不支持安全区域属性时,使用兜底的 padding-top: 25PX 样式(大写的 PX 是为了不被插件转义成 vw 或 rem),但上述的 Android 设备中,兜底样式也不会生效:body { /* prettier-ignore */ padding-top: 25PX; padding-top: constant(safe-area-inset-top); padding-top: env(safe-area-inset-top); } 那么如何解决这个问题呢? 3. 解决方案 # 这里我们就要借助 js 来实现了。首先我们向页面中插入一个看不见的 div,将 div 的高度设置为安全距离的高度,然后再通过 js 获取其高度,若高度为 0,则说明没有生效。let status = 0; // 0:还没数据,-1:不支持,1:支持 /** * 判断当前设置是否支持constant(safe-area-inset-top)或env(safe-area-inset-top); * 部分Android设备,可以认识safa-area-inset-top,但会将其识别为0 * @returns {boolean} 当前设备是否支持安全距离 */ const supportSafeArea = (): boolean => { if (status !== 0) { // 缓存数据,只向 body 插入一次 dom 即可 return status === 1; } const div = document.createElement('div'); const id = 'test-check-safe-area'; const styles = [ 'position: fixed', 'z-index: -1', 'height: constant(safe-area-inset-top)', 'height: env(safe-area-inset-top)', ]; div.style.cssText = styles.join(';'); div.id = id; document.body.appendChild(div); const areaDiv = document.getElementById(id); if (areaDiv) { status = areaDiv.offsetHeight > 0 ? 1 : -1; // 该 div 的高度是否为 0 areaDiv.parentNode?.removeChild(areaDiv); } return status === 1; }; 那么在已经设置了安全区域属性的地方,都需要额外执行下 supportSafeArea()方法:const SignTaskDetail = () => { const [safaArea, setSafeArea] = useState(true); // 当前页面是否支持 safe-area-inset-top useEffect(() => { setSafeArea(supportSafeArea()); }, []); return ( ); }; 4. 总结 # 设备兼容性一直我们前端在解决的问题,无论是在 PC 端还是在移动端,浏览器的多样性和编程语言的发展,必然需要解决这些问题。
2024年10月20日
7 阅读
0 评论
0 点赞
1
...
44
45
46
...
213