小天管理 发表的所有内容
-
我哥家里是用蓝牙遥控器的夏普电视,因此不支持小爱音箱控制。买了 appletv ,每次看电视,开关机和调音量都必须要用电视本身的蓝牙遥控器,然后再换到 appletv 遥控器,用起来很麻烦。 所以我的问题来了: 这种本身是蓝牙遥控的电视,有办法可以改成红外遥控的吗,然后就支持让小爱音箱控制,也支持让 appletv 遥控器控制电视和 appletv ,无需来回切换。
-
正在开发一个 app ,用户在使用 app 访问服务时,需要根据对应的服务从后端获取对应的配置文件( json 格式)。 一共有上千个独立的配置项。 目前是当 app 第一次启动时,会首先通过接口查询配置项清单,然后再依次对各配置项进行请求获取。 这样的问题是,一个 app 就会向后端发起上千个请求。而且可能需要十来分钟甚至更长时间才能把全部配置拉下来。 这样一方面对后端服务器造成压力,另一方面影响用户体验。 如果把配置全部打包在一起的话,大概40-50MB左右。 有些配置项还会更新,这就需要app 在后续的运行过程中对有更新的配置项进行更新。 请问大佬们有什么好的思路?
-
1, 直播 App 上线非常复杂吗?客户刚刚新注册的公司,能否申请下来相关资质? 2 ,是否有开源产品可以替代的,私有化部署的直播 App ,自己搭建服务器,内部人员使用。 **说明,不涉及违法的事情,就是客户方不希望走公共直播平台。 希望有经验的朋友 给指点一二,谢谢!
-
简约版: 公司做海外业务的,现做一款游戏推荐平台,海外 8 个国家上线,30 多万日活; 公司已盈利,资金健康。 需求: 2~5 年经验产品经理,希望有 C 端经验,对 web3 、AI 感兴趣或有实际参与经验的加分; 薪资:15~30k ,具体视经验、匹配度、潜力评估; 有意者联系: V:MTM1MTA3MDk1MzE=( base64 )
-
要去欧洲出差几个月,想要自己弄一个 VPS 线路回国,想征求一下可行性、稳定性以及对应的法律风险 有朋友说回国的 VPN 服务不会被 ban ,那是不是可以直接 wirequard 或者最传统的 vpn 方案 如果不行,使用现在主流的协议,用 xui 面板弄一个 vless/shadowsocks 方案是否可行 最后的备选方案是 Trojan 自用,准备使用宁波双向 20m 的 vps 搭建
-
2023.3 月 pdd 买的 macbook pro 2021 。 2023.12 月 发现无法充电,在保修内更换了电池和主板。 2024.9 月 又发现无法充电了。 所以,每隔 8-9 个月,电池就废了? 很无奈啊。
-
如题,目前正在使用的是一把固定的椅子,就是海绵坐垫和海绵靠背,别的什么都没有,我坐起来也并没有什么不舒服的地方。所以想问一下大家。动辄上千块的人体工学椅究竟好在哪里?相同功能的椅子,PDD 上没有品牌只要 200 块,带上品牌就要 500 块。这其中的差价抛开材质和做工,在坐的体验上究竟能有多少提升?功能更多的椅子 2000 块也是有的,前天下午我坐着 20 块买的较高的马扎打了一下午游戏也并没有觉得有什么不妥,身体也没有产生不舒服的感觉( 30 岁了)。之前去家具城也体验了各种价位的人体工学椅,我的感觉是没有感觉。椅子的本质就是坐,我可以理解材质做工功能的提升进而价格提升,但是以人体工学为宣传噱头、以身体健康来制造焦虑,这样的价格提升除去有钱人怎么样的人会去买单呢?我是穷卫(鬼),轻喷。
-
双卡 iPhone 14 Pro Max 的兄弟们,主卡电信,副卡移动,朋友打你卡 2 (移动)的时候会不会偶尔听不清晰 不知道大家会不会有这样,当时当你主卡电信,副卡移动的时候,都是满信号并且信号覆盖不错的情况下,朋友打你卡 2 移动的时候,说你这边的声音怎么听不清晰。 我是 14PM ,系统是首发初始系统 iOS 16.0 从未升级过的 我是卡 1 电信主力,卡 2 移动(保号)专门打家庭短号网的
-
各位使用成都、四川移动地区专线的 V 友们,请关注自己专线的网络状况: 由于打压 PCDN ,从 2024.9.1 日下午 16:00 以后,成都地区移动出现了只要有上传流量(流速>10mbps ),会出现到网关的高 ping 、跳包现象。同时上传速度仅仅只有正常速度的 10/1 。 我这里已经有两条专线中招,有一条日均上传很高(作用于网盘备份、同步),而另外一条日均上传不过 5-15G ,一个月下下上行流量不过 137G 。 现在的情况就是:下行可以跑满合同限速,ping 不丢包,只要测速开始测试上传,并且速率>10m ,到第一跳网关将出现高 ping 导致整个网络连接质量极差,停止上传后网络会立即恢复正常。 tips:1 、这些都是固定 IP 的专线,不是家宽; 2 、最开始我还认为只针对高上传用户,结果低上传用户也被一锅端了。 3 、不跑 PCDN 、PT ,不要拿跑 PCDN 被收拾来幸灾乐祸。这个 QOS 策略纯纯傻逼。 大家对照一下检查一下专线的情况把 图片: https://imgur.com/a/xz6kYwV
-
最近提了离职,因为工资和工作环境问题,刚才和老板谈工资清算没谈拢… 入职说好的 5%的项目提成,我这边的工作已经做完了,但是现在项目还没有验收,所以貌似公司不打算给我….这样可以吗? 我提 N+1 的时候又说要满一年才有,但是劳动法规定的 6-12 个月是按一年算的吧? 如果谈不拢有必要走劳动仲裁吗? 还是我有问题?
-
我的域名是很久以前通过 cf partner 面板接入的 cname 记录。仅接入了子域名 cdn.a.com(源站 b.com),没有接入 a.com 和 www.a.com。接入时的设置是通过 http 续签证书。也就是在根目录下设置 http://cdn.a.com/.well-known/pki-validation/caxxxxx.txt 文件,cf 安排续签证书时校验这个文件验证所有权。 前天突然发现无法通过 https 访问 cdn.a.com。http 访问则没有问题。直接 https 访问源站 b.com 也没有问题。 通过 openssl s_client -connect cdn.a.com:443 -showcerts 检查发现没有证书: CONNECTED(00000003) 140170526717840:error:14077410:SSL routines:SSL23_GET_SERVER_HELLO:sslv3 alert handshake failure:s23_clnt.c:769: no peer certificate available 登录 cf 后台,在 [网站] [ SSL/TLS ] [边缘证书] 页面中,边缘证书列表显示为无证书: 我已检查过: [ SSL/TLS ] 被设置为完整(严格)。 没有开启 [始终使用 HTTPS ] 。确保续签时可通过 http 访问校验文件。 [安全性] [ WAF ] 中已设置规则,放行对校验文件的访问。 外部 DNS 解析服务中 cdn.a.com 通过 cname 指向官方指定的 cdn.a.com.cdn.cloudflare.net。 curl http://cdn.a.com/.well-known/pki-validation/caxxxxx.txt 可以得到预设的值。 我已试过如下方法但都没有效果: cf 后台暂停该域名,一小时后重启。 [ SSL/TLS ] 调整为关闭,一小时后恢复为完整(严格)。 [ SSL/TLS ] 调整为关闭,一小时后恢复为完整。 [边缘证书] 中关闭 [通用 SSL ] ,一小时后启用。 按下图提示,在外部 DNS 解析服务中设置 _acme-challenge.cdn.a.com cname 指向 cdn.a.com.uuid.dcv.cloudflare.com,然后关闭 [ SSL\TLS ] 和 [通用 SSL ] 十几分钟后恢复。
-
![1000013040.webp]( https://j.teno.cx/root/2024/09/01/66d47e29cf906.webp) 虽然之前有人发了,但实在太神奇所以再补充下,原理可以看参考资料:,https://www.v2ex.com/t/1014864 方法就是打开 prefix hint 功能, ![1000013041.webp]( https://j.teno.cx/root/2024/09/01/66d47f8d48c53.webp) 输入当前 IP 前缀或者按照 bras 的前缀写全尝试也可以,也就是可以调靓号,也可以全 0 补全,(如图上就是 0 补全)如果报错就是 bras 拒绝了手动的前缀 实测本地移动全 0 最短前缀可以,/48 ~/60 的子网部分 随便改靓号,电信如果改的太短或者太靓号会强制修改几个位,没法全 0 最短前缀,但只要分配给我了就可以一直用 # 此方法修改后重拨号前缀固定,理论上只要 bras 方这段前缀不过期就可以一直用,算静态 ipv6 体验卡 关于如何 win 系统用短 ipv6 , win 系统要把 ipv6 临时地址和 ra 通告给禁用了,适配器里面再改成手动获取 ipv6 地址 实现 12 位超短 ipv6 地址 linux 系统更简单,直接配置静态 ipv6 即可, 安卓只支持 slaac 短不了一点,iOS MacOS 似乎没法静态 ipv6 ,用有状态 dhcpv6 可以分固定 v6 ,(但 ROS 不支持有状态)
-
之前 pt 一直是直接使用群晖的 ds ,前一阵子看到 mp 感觉可以整自动化一些。但发现 mp 需要特定网站认证,而我只有 mt ,无法认证。 故求下列网站的一个要求 iyuu/hhclub/audiences/hddolby/zmpt/freefarm/hdfans/wintersakura/leaves/ptba /icc2022/xingtan/ptvicomo/agsvpt/hdkyl/qingwa/discfan/haidan/rousi 保证完成新手任务。 这是我 mt 的数据 上傳量: 4.06 TB 下載量: 3.13 TB 做種/下載時間比率: 19.59 邮箱地址:bGltY2hpaGlAZ21haWwuY29t ( base64 )
-
朋友有一台 MacBook Air 2018 ,可能升级 Sonoma14.6.1 时候进度条卡住了一个小时,被强制关机了。 结果,无法正常开机,Apple 进度条走完之后,就被要求输入密码来解锁。然后输入 Mac 密码后又无限循环 logo 进度条输密码… 1 、首先尝试 Mac 自带恢复模式进行升级操作,尴尬的是磁盘空间不足差了 3GB ,没辙。 2 、于是尝试用制作 U 盘启动器的方式来升级,但是选择升级过程中报错「这个“安装 macOS Sonoma”应用程序副本已损坏,不能用来安装 macOS 」,终端设置 date 时间无效。 3 、重置 PRAM/NVRAM 无效。 4 、shift 按住进入安全模式无效。 5 、无时间机器备份 目前是不是除了抹掉磁盘以外没招了?
-
1 、如在 PCB 板和原理图中用了( XH2.5-2A 直针 2PIN 针座),要找适配它的母头元件准备加入到清单,怎么找最便捷、准确? 这个自己瞎摸索过,是不是除了历史以来熟悉搭配型号,只能手选,别无它法? 2 、在自动布线的时候,限定某些区域不走线,不产生走线过孔,比较灵巧方式是如何做? 是不是用个什么容易放置、删除的东西“占位”,自动补完线后删除占位内容? 3 、以前看别人做电子制作,在一整块 PCB 板上画出一部分区域(连板?),可以像梳打饼干那样折断这部分区域,成为单独的 PCB 板块,这种划开区域的做法,在立创 EDA 里怎么做? 1.6MM 厚的板子。。。
-
如题,坐标广州,开的电信,应该是 500Mb 的带宽,不管是有线连接的台式机+PS5 ,还是我的 wifi 连接的笔记本,网速都很快,下载也从来没问题,但是手机网络就经常出问题,包括但不限于微信、飞书图片半天刷不出来(我的使用体验),微博、小红书的图片和视频也加载特别慢(我对象的使用体验)等。很奇怪啊,有大佬了解吗? 路由器是 RT-AX88U Pro 提前拜谢大佬!
-
在甲骨文免费的机器上用 docker 运行着一个 vaultwarden 服务,用来管理自己的所有密码。 因为是免费的 vps ,为了避免机器被忽然回收导致数据丢失,因此需要定期备份 vaultwarden 的数据库。 首先 vaultwarden 使用了 docker compose 部署,部署文件如下: services: vaultwarden: image: vaultwarden/server:latest container_name: vaultwarden restart: always environment: SIGNUPS_ALLOWED: false # Deactivate this with "false" after you have created your account so that no strangers can register volumes: - ./vw-data:/data # the path before the : can be changed ports: - 11001:80 # you can replace the 11001 with your preferred port 因此在 vw-data 下面,保存着所有密码数据,只要备份这个目录,就可以还原。 备份工具使用 restic ,它支持将数据直接备份到远程,下面步骤主要是设置好备份地址,然后定期执行的操作细节: 一: 首先安装 restic sudo apt update sudo apt install restic 二: 初始化 restic 仓库 先设置好环境变量: export RESTIC_REPOSITORY=s3:https://<minio-host>:<minio-port>/<bucket-name>/<subpath> export AWS_ACCESS_KEY_ID=<your-access-key> export AWS_SECRET_ACCESS_KEY=<your-secret-key> export RESTIC_PASSWORD=<your-password> 然后执行: restic init 执行成功后,会在远程 S3 目录 https://<minio-host>:<minio-port>/<bucket-name>/<subpath> 看到一些初始化 meta 数据。 三: 创建备份并清理旧数据的脚本 backup_and_prune.sh ,内容如下: #!/bin/bash # 设置环境变量 export RESTIC_REPOSITORY=s3:https://<minio-host>:<minio-port>/<bucket-name>/<subpath> export AWS_ACCESS_KEY_ID=<your-access-key> export AWS_SECRET_ACCESS_KEY=<your-secret-key> export RESTIC_PASSWORD=<your-password> # 备份目录 BACKUP_SOURCE="/home/ubuntu/vaultwarden/vwdata" # 日志文件 LOG_FILE="/home/ubuntu/back.log" # 执行备份并记录日志 restic backup $BACKUP_SOURCE >> $LOG_FILE 2>&1 # 删除超过 7 天的旧备份并记录日志 restic forget --keep-daily 7 --prune >> $LOG_FILE 2>&1 # 记录快照信息 restic snapshots >> $LOG_FILE 2>&1 请自行将里面的变量和路径替换为实际值。 四: 设置定期执行: 使用 cron 来设置每天定期执行备份任务。 crontab -e 添加以下行来设置每天凌晨 2 点执行备份脚本: 0 2 * * * /home/ubuntu/backup_and_prune.sh 确保你的脚本有执行权限: chmod +x /home/ubuntu/backup_and_prune.sh 这样你的备份脚本就会每天凌晨 2 点执行一次了。 五: 验证备份 验证数据是否正常的最好办法是让 vaultwarden 加载备份的数据,然后在网页查看数据是否是最新的。因此接下来的操作是将远程的备份下载到本地,然后在本地执行的具体操作。 首先设置环境变量 export RESTIC_REPOSITORY=s3:https://<minio-host>:<minio-port>/<bucket-name>/<subpath> export AWS_ACCESS_KEY_ID=<your-access-key> export AWS_SECRET_ACCESS_KEY=<your-secret-key> export RESTIC_PASSWORD=<your-password> 列出所有快照: restic snapshots 恢复某个快照到指定目录: restic restore <snapshot-id> --target ~/Downloads/ 根据本文的例子,最终的文件还原到了 ~/Downloads/vw-data 下面。 本地临时运行一个 docker compose 文件: services: vaultwarden: image: vaultwarden/server:latest container_name: vaultwarden restart: always environment: SIGNUPS_ALLOWED: false # Deactivate this with "false" after you have created your account so that no strangers can register volumes: - ~/Downloads/vw-data:/data # the path before the : can be changed ports: - 11001:80 # you can replace the 11001 with your preferred port docker compose up 启动后,访问 localhost:11001 查看数据正常就表示备份没有问题了。 本文首发于: https://blog.tomyail.com/backup-vaultwarden-data-using-restic/ 转载请注明出处
-
前言 最近更新了我的系列文章,其中有一部分是关于 JavaScript 语言中“点表示法”的使用。 文章中有 5 个工具函数是纯JavaScript的,我觉得不仅仅是小程序项目用得上,其他前端 JS 项目也应该用得上。 我把文章中的这一部分单独整理出来,给需要写前端代码的朋友参考参考。 你可以直接在 Github 项目 中的前端 utils.js 文件中找到这 5 个工具函数。 下面是文章节选,完整的文章列表可以在 Github 项目 中查看。 点表示法a.b.c 用点表示法给对象赋值:putValue函数 想象一下,如果a是一个对象,你要给a.b.c.d赋值为 1 ,你会这样写? let a // 通过某种方式获得的对象 if (!a.b) { a.b = {} } if (!a.b.c) { a.b.c = {} } a.b.c.d = 1 拜托,大学生才这样写。为此,我们引入了putValue函数: utils.putValue(a, 'b.c.d', 1) putValue函数会自动创建a.b、a.b.c中间对象,它的声明如下: /** * 向对象中按照路径赋值,如果路径上的中间对象不存在,则自动创建。 * @param {Object} obj - 目标对象。 * @param {string} key - 属性路径,支持'a.b.c'形式。 * @param {*} value - 要设置的值。 * @param {Object} [options={}] - 可选参数。 * - {boolean} remove_undefined - 如果为 true 且 value 为 undefined ,则删除该属性。 * @throws {Error} 如果 obj 为 null 或 undefined ,或路径不合法(如中间非对象)则抛出异常。 */ putValue(obj, key, value, {remove_undefined = true} = {}){ // ... } 使用这个函数有两个地方要注意,一个是如果路径上的中间对象不是对象,会抛出异常。例如a.b=2,这里b不是对象,此时会抛出异常。 另一个是如果value是undefined,则会删除该属性。例如下面的代码: utils.putValue(a, 'b.c.d', undefined) console.log(a) // {b: {c: {}}},putValue 会自动创建中间对象,但不会自动删除空对象 但若你真想赋值为undefined,可设置remove_undefined参数为false。 读取对象属性值:pickValue函数 对应的,如果要获取a.b.c.d的值,可以使用pickValue函数: utils.putValue(a, 'b.c.d', 1) const value = utils.pickValue(a, 'b.c.d') console.log(value) // 1 当然你也可以直接使用javascript的原生语法: const value = a.b?.c?.d 这两种写法,当中间路径不存在时,均会返回undefined。 但a.b?.c?.d这种写法是硬编码,而在实际开发中路径可能是动态的。例如用户要修改一个配置项,这个配置可能是user_config.font.size也可能是user_config.page.color.background,如果使用硬编码的方式,可能会写出这样的代码: if (key === 'font.size') { user_config.font.size = value } else if (key === 'page.color.background') { user_config.page.color.background = value } // 更多的 if 语句... 这样写显然不够优雅,看看putValue与pickValue的组合用法。 // 写入用户配置: utils.putValue(user_config, key, value) // 读取用户配置 const value = utils.pickValue(user_config, key) 不管key怎么变,一句话搞定,感觉一下子和大学生拉开差距了是吧? 向数组末尾添加元素:pushValue函数 在实际开发中,常有向数组末尾添加数据的需求。例如记录用户最近的评论,此时你可以使用pushValue函数: const user_data = {} // 用户数据 let comment = {content: '顶'} // 用户的评论 utils.pushValue(user_data, `articles.recent_comments`, comment) console.log(user_data) // {articles: {recent_comments: [{content: '顶'}]}} 在上面代码中,pushValue函数先是自动创建了user_data.articles.recent_comments数组,然后把comment添加到数组末尾。pushValue函数的声明如下: /** * 将值推入对象指定路径的数组中,若路径或数组不存在则自动创建。 * @param {Object} obj - 目标对象。 * @param {string} key - 数组属性的路径,支持'a.b.c'形式。 * @param {*} value - 要推入的值。 * @throws {Error} 如果路径不是数组,则抛出异常。 */ pushValue(obj, key, value){ // ... } 再次调用此函数,数组中就会有两个评论: comment = {content: '再顶'} utils.pushValue(user_data, `articles.recent_comments`, comment) console.log(user_data) // {articles: {recent_comments: [{content: '顶'}, {content: '再顶'}]}} 向对象中添加多个属性:putObj函数 前面我们使用putValue函数向obj对象写入了一个属性值,但如果你要写入很多个(例如 100 个)属性值,你可能会使用for循环: let user_config = {} let new_config_keys // 100 个新的配置项(数组) let new_config_values // 对应的 100 个值(数组) for (let i = 0; i < new_config_keys.length; i++) { utils.putValue(user_config, new_config_keys[i], new_config_values[i]) } 这样写没问题,但在实战中,你拿到的用户配置往往不是数组的形式,而很可能是一个对象,例如: let new_config = { font: { size: 16, 'family.first': 'Arial', 'family.second': 'sans-serif', }, 'page.color.background': '#fff', // ... } // 你对拿到的配置数据又进一步处理 new_config.update_time = new Date() 这种情况下你可以使用putObj函数一次性写入多个属性值: utils.putObj(user_config, new_config) console.log(user_config) /* 输出如下: { font: { size: 16, family: { first: 'Arial', second: 'sans-serif' } }, page: { color: { background: '#fff' } }, update_time: '...', } */ 注意putObj会自动处理上面new_config变量中各种路径的写法。putObj函数的声明如下: /** * 将一个对象的所有属性按路径添加到另一个对象中。 * @param {Object} obj - 目标对象。 * @param {Object} obj_value - 要添加的属性对象,键支持'a.b.c'形式的路径。 * @param {Object} [options={}] - 可选参数。 * - {boolean} remove_undefined - 如果为 true 且 value 为 undefined ,则删除该属性。 * @throws {Error} 如果 obj 为 null 或 undefined ,或路径不合法(如中间非对象)则抛出异常。 * * 注意 * 若 obj_value 中出现重复路径,则后者会覆盖前者。 * 如 obj_value = {a: {b: 1}, 'a.b': 2},则结果为 {a: {b: 2}} */ putObj(obj, obj_value, { remove_undefined = true} = {}) { // ... } 从对象中获取多个属性:pickObj函数 同样的,我们可以一次性读取多个对象的属性值。例如虽然小程序中的用户配置非常复杂,但当前页面仅关注背景颜色、字体大小等少量配置项,你可以这样使用pickObj函数: let user_config // 某个用户的所有配置 // 本页面需要关注的配置 const keys = ['page.color.background', 'font.size', 'font.family'] // 获取当前页面需要的配置 const curr_config = utils.pickObj(user_config, keys) console.log(curr_config) /* 输出如下: { 'page.color.background': '#fff', 'font.size': 16, 'font.family': { first: 'Arial', second: 'sans-serif' } } */ console.log(curr_config.font) // undefined 注意,传给pickObj函数的第二个参数是一个字符串数组,而不是对象。并且,pickObj返回的对象中,属性值不是以curr_config.font.size这样的形式返回,而是返回curr_config['font.size']。 当然,如果你想要curr_config.font.size这样的形式,可用putObj转换一下: let obj_config = utils.putObj({}, curr_config) console.log(obj_config.font.size) // 16 点表示法在微信小程序中实战演示 为什么要设计这几个函数?为什么要支持config.a.b.c与config['a.b.c']两种写法混用?为什么传给putObj的第二个参数是对象,而传给pickObj的第二个参数是字符串数组?为什么pickObj返回的对象属性值不是config.a.b.c这样的形式,而是config['a.b.c']? 因为这样设计符合实战需求,一句话解释就是:“这样好用”。 下面我们通过几个案例来演示这些函数在实战中的应用。 在 js 中设置用户配置 假设用户首次打开小程序,你需要设置用户默认字体大小为 16 ,背景颜色为白色。可以这样写: let user_config = {} utils.putValue(user_config, 'font.size', 16) utils.putValue(user_config, 'page.color.background', '#fff') 使用putValue设置后,你想修改字体大小和背景颜色?可以这样写: user_config.font.size = 18 user_config.page.color.background = '#000' 在 wxml 中实现修改用户配置 你可能会在 wxml 页面中实现多个配置项的修改,并且使用同一个函数来处理。这时你可以这样写: <button bind:tap="changeConfig" data-key="font.size" value="16" > <button bind:tap="changeConfig" data-key="page.color.background" value="#fff"> changeConfig(e){ const { user_config } = this.data const { key, value } = e.currentTarget.dataset // 从 wxml 中获得点表示法的 key 字符串,直接调用 putValue 函数 utils.putValue(user_config, key, value) // 修改背景色时顺便改一下字体颜色(两种写法混用) if (key === 'page.color.background' && value === '#fff') { user_config.page.color.font_color = '#000' } // 记录最近修改时间 user_config.update_time = new Date() } 在 wxml 中使用用户配置 要在页面中使用page.color.background与page.color.font_color的值,实现根据用户配置显示不同的颜色,可以这样写: <view style="background-color: {{color.background}}"> <text style="color: {{color.font_color}}"> Hello, WxMpCloudBooster! </text> </view> onLoad(){ const { user_config } = this.data const color = utils.pickValue(user_config, 'page.color') this.setData({color}) } 你看,我们传递给pickValue的key根据实际需求可长可短。 初始化默认的用户配置 你希望为每个用户设置一个默认的用户配置,并且你想用常规方式写(不使用点表示法)。可以这样: // 默认配置 const DEFAULT_CONFIG = { font: { size: 16, }, page: { color: { background: '#fff', font_color: '#000' } }, // 这里也可以使用点表示法 a.b.c ,但你不想这样写... } App({ initConfig(){ let { user_config } = this.data utils.putObj(user_config, DEFAULT_CONFIG) // user_config 的其他值会被保留 // 保存用户配置... } }) 注意,上面代码中user_config可能会有其他没有出现在DEFAULT_CONFIG中的配置项,这些配置项会被保留。 记录用户最近发表的内容 假如你已经实现了“记录用户最近发布的评论”功能,代码如下: <button bind:tap="append" data-key="articles.recent_comments" data-prop="comment" > 当用户点击这个按钮时,假设this.data中已经有一个comment对象,你可以这样添加评论: append(e){ const { user_data } = this.data const { key, prop } = e.currentTarget.dataset const value = this.data[prop] // prop === "comment" utils.pushValue(user_data, key, value) // 注意这里用的是 push } 上面这个append函数会把this.data.comment对象添加到user_data.articles.recent_comments数组的末尾。 然后,此时你希望再增加一个按钮,可以把最近的点赞数据this.data.like添加到user_data.articles.recent_likes数组的末尾,那么只需一句: <button bind:tap="append" data-key="articles.recent_likes" data-prop="like" > 完成了,你不需要修改append函数,只需要给data-key和data-prop属性设置不同的值即可。 可见,点表示法很大的目的是为了在wxml中可以方便地指定路径,并在js中方便地处理这些路径。 在页面中修改多个配置项 假设你有一个修改用户配置项的页面,wxml代码如下: <!-- 注意这里有一个 for 循环 --> <view wx:for="{{configs}}"> 配置名称:{{item.title}} 当前值:{{item.value}} 输入新值:<input type="text" /> 点击修改:<button bind:tap="changeConfig"/> </view> 上面代码使用了for循环,configs变量中有多少个值,就会显示多少个配置项。 为了实现在用户打开页面时显示的是用户的当前值(而不是默认值),你还需要从user_config中读取当前用户的配置值。 代码样例如下: // 代码中写死了需要修改的配置项以及默认值 configs = [ {title: '字体大小', key: 'font.size', value: 16}, {title: '背景颜色', key: 'page.color.background', value: '#fff'}, {title: '字体颜色', key: 'page.color.font_color', value: '#000'}, ] // 读取当前用户的配置值 const uc_obj = utils.pickObj(user_config, configs.map(item => item.key)) // 注意,这里的 uc_obj 是 uc_obj['font.size'] 这样的形式,而不是 uc_obj.font.size // 用户当前值覆盖默认值 configs.forEach(item => { item.value = uc_obj[item.key] }) this.setData({configs}) // 传给 wxml 页面显示 这样你就实现了修改多个配置项的页面,用户打开页面时显示的是用户当前的配置值。 提问:假设我们坚决不使用点表示法,且要实现上面这些功能,你要如何设计才能如此简单、高效? 让你的函数也支持点表示法 好了,目前我们花了不少篇幅介绍点表示法,这是因为后面我们会介绍更多的工具函数,而这些工具函数都支持点表示法的调用方式。 当你编写自己的工具函数时,你可以调用putValue、pickValue、pushValue、putObj、pickObj这 5 个函数,轻松地让你的工具函数也支持点表示法。如果你不知道如何实现,可以参考utils.js中其他函数的代码。 (文章节选完,如果你感兴趣的话可以看看 Github 项目)
-
不同版本的 gradle 指令改来改去,就算这是两年一改也很是折腾,另外,为什么包管理器一定要绑定 jdk 版本呢 python 同样一个包,兼容 3.8 而与 3.12 冲突? semVer 说好的小版本兼容呢?另外,项目 a 依赖的 b 跟 c 的上层依赖互相冲突,只好分两个 project 搞
-
Spring RestTemplate 拦截器修改请求体导致的诡异问题 最近在工作中发现了 Spring 的一个"特性"(也许可以叫 Bug ?),反正我已经给 Spring 提了 PR ,等着看能不能合进去。 问题背景 最近在调用第三方 API 时,遇到了一个有意思的场景。整个调用流程大概是这样的: 先调用 /login 接口,发送 username 和 password ,对方服务返回一个 JWT 。 之后的每个请求接口都是标准格式,需要把 JWT 和请求参数放到一个 JSON 中,类似这样: { "token": "JWT-TOKENxxxxxx", "data": { "key1": "value1", "key2": "value2" } } 发送请求,然后拿到响应报文。 解决方案 为了避免在每个接口都重复封装 token ,我想到了用 org.springframework.http.client.ClientHttpRequestInterceptor 来拦截请求,统一修改请求体。 代码大概长这样: this.restTemplate = new RestTemplateBuilder() .requestFactory(() -> new ReactorNettyClientRequestFactory()) .interceptors((request, body, execution) -> { byte[] newBody = addToken(body); // 调用登陆获取 token ,修改入参 body ,添加 token return execution.execute(request, newBody); }) .build(); 诡异的问题 修改完成后,进入测试阶段,奇怪的事情就发生了:token 能正确获取,body 也修改成功了,但对方的接口一直报 400 ,Invalid JSON 。更奇葩的是,我把 newBody 整个复制出来,用独立的 Main 代码发送请求,居然一次就成功了。 深入源码 不服气的我只能往源码里找原因。从RestTemplate一路 Debug 到org.springframework.http.client.InterceptingClientHttpRequest.InterceptingRequestExecution#execute,发现了这么一段代码: @Override public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException { if (this.iterator.hasNext()) { //这里是在执行 interceptor 链,我的登陆和修改 body 接口就在这里执行 ClientHttpRequestInterceptor nextInterceptor = this.iterator.next(); return nextInterceptor.intercept(request, body, this); } else { // 上面的 interceptor 链执行完后,下面就是真实执行发送请求逻辑 HttpMethod method = request.getMethod(); ClientHttpRequest delegate = requestFactory.createRequest(request.getURI(), method); request.getHeaders().forEach((key, value) -> delegate.getHeaders().addAll(key, value)); if (body.length > 0) { if (delegate instanceof StreamingHttpOutputMessage streamingOutputMessage) { streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() { @Override public void writeTo(OutputStream outputStream) throws IOException { StreamUtils.copy(body, outputStream); } @Override public boolean repeatable() { return true; } }); } else { StreamUtils.copy(body, delegate.getBody()); } } return delegate.execute(); } } 在 Debug 到request.getHeaders().forEach这里时,我突然发现 request 里的Content-Length居然和body.length(被修改后的请求体)不一样。 问题根源 继续往上追溯,在org.springframework.http.client.AbstractBufferingClientHttpRequest中找到了这段代码: @Override protected ClientHttpResponse executeInternal(HttpHeaders headers) throws IOException { byte[] bytes = this.bufferedOutput.toByteArrayUnsafe(); if (headers.getContentLength() < 0) { headers.setContentLength(bytes.length); } ClientHttpResponse result = executeInternal(headers, bytes); this.bufferedOutput.reset(); return result; } 原来Content-Length在执行拦截器之前就已经被设置了。但我们在拦截器里修改了body,导致对方接收到的 JSON 格式总是不对,因为Content-Length和实际的请求体长度不匹配。 解决问题 这时候为了先解决问题,就先在interceptor中重新赋值了Content-Length this.restTemplate = new RestTemplateBuilder() .requestFactory(() -> new ReactorNettyClientRequestFactory()) .interceptors((request, body, execution) -> { byte[] newBody = addToken(body); // 调用登陆获取 token ,修改入参 body ,添加 token request.getHeaders().setContentLength(body.length); // 重新设置 Content-Length return execution.execute(request, newBody); }) .build(); 测试后,问题解决了。 反思和改进 问题虽然解决了,但我琢磨了一下,虽然是我在拦截器中修改了 body ,但这个地方 Spring 应该还是有责任把错误的Content-Length修正的。 第一,Spring 的文档中没有明确写这里应该由谁来负责,是个灰色地带。 第二,我们用RestTemplate谁会自己设置Content-Length啊,不都是框架设置的吗,所以这里不也应该由框架来负责嘛。 思考完,周末找了个时间给 Spring 提了个 PR ,有兴趣的同学可以到这里看看。Update Content-Length when body changed by Interceptor 有一说一,虽然不是第一次提 PR ,但是还是感觉挺爽的,记录一下。 写的挺乱的,技术一般,大佬轻喷。