Host 配置决定了你访问一个域名时 surge 如何解析这个域名的 DNS 记录,想获得高效安全的 DNS 解析体验,首先需要先根据你所处的网络环境配置合适的 Host 规则,先看一个配置示例:
[Host]
local.example.net = 127.0.0.1
local.xxx.net = 1.1.1.1
*xxxx-inc.com = server:system
*example*.com = server:system
*.* = server:https://223.6.6.6/dns-query
各种规则可以总结归类为三个梯队,优先级从上到下:
需要注意的是,只有 server:system 才会遵守本地 /etc/hosts 文件里的映射,以后如果遇到配置了 /etc/hosts 不生效,可以检查对应域名是否在 surge 里指定了其他服务器解析。
对于希望走代理的请求,应该尽量避免它们在本地触发 DNS 解析,最好将你常用的需要代理的网站加到 surge 的规则列表中,例如:
[Rule]
DOMAIN-SUFFIX,baidu.com,📡 Proxy
DOMAIN-SUFFIX,qq.com,📡 Proxy
如果你有大量域名需要代理,可以使用 Surge 提供的 rule-set 或者 domain-set 将这些规则和域名维护到单独的文件中,通过 URL 来引用和自动更新,例如:
# 自定义代理域名
RULE-SET,https://ruleset.example.com/proxy.txt,📡 Proxy
# GitHub 项目维护的代理域名
DOMAIN-SET,https://cdn.jsdelivr.net/gh/Loyalsoldier/surge-rules@release/proxy.txt,📡 Proxy
按照上述设置后,访问这些域名将不会在本地触发 DNS 解析,请求会直接发送到代理服务器,由它负责 DNS 解析,这样既可以提升隐私也可以获得更好的 DNS 解析结果。
好的 surge 配置应该尽量避免在前面的规则触发 DNS 解析,将需要触发 DNS 的规则尽量放置的末尾,下面是一个示例:
DOMAIN-SUFFIX,github.com,📡 Proxy
PROCESS-NAME,go-ipfs*,📡 Proxy
USER-AGENT,Figma,📡 Proxy
IP-CIDR,66.197.128.0/17,📡 Proxy,no-resolve
IP-CIDR,23.246.0.0/12,DIRECT
RULE-SET,LAN,DIRECT
GEOIP,CN,DIRECT
要获得最佳阅读体验,请访问原文 https://baiyun.me/surge-dns-optimization-guide]]>
本文适合有以下需求的读者:
在接触到 RIME 之前,我用过很多款中文输入法,从最早的搜狗、到后来的百度、必应Bing输入法(后来改名叫微软拼音输入法)。其中 Windows 上最满意的是必应输入法,无奈 Mac 平台用不了。后来 Mac 上一直使用了好多年百度输入法,打字词库总是缺少一种称手的感觉,又找不到好的代替。
另外考虑到这类国内厂商的输入法基本毫无隐私和安全可言,它们都是免费产品,如果不是为了获取用户数据,那他们开发和持续维护这些输入法的目的是什么呢?
Rime(中州韵)是一个开源的输入法引擎,鼠须管是基于 Rime 构建的一款开源输入法,面向 macOS 平台。
我很早就听说过这款输入法的大名,也遇到很多人推荐它,但是被它繁杂的配置劝退了。最近,看到有人做了一套配置集合,整合了很多常用的功能,配合官方的配置管理工具让使用门槛一下子降低了好多。
接下来是一个简明指南,可以帮你快速安装和配置好一个开箱即用的输入法。
继续阅读需要的一些前置要求:
# 回到家目录,可选
cd ~
# 安装 plum,这是 Rime 官方的配置管理工具
git clone --depth=1 https://github.com/rime/plum
cd plum
# 安装 Rime 配置:雾凇拼音
bash rime-install iDvel/rime-ice:others/recipes/full
# 安装 鼠须管 Squirrel,安装之后需要登出系统重新登入才能添加输入法
brew install --cask squirrel
下面是雾凇拼音提供的功能概览图
鼠须管和默认配置安装好之后,其实已经可以正常使用了。本节的示例教你如何正确的做一些个性化配置。
执行 vim ~/Library/Rime/default.custom.yaml
,填入以下配置,下面的配置主要做了两个事情:
# Rime Patch settings
# encoding: utf-8
patch:
# 方案列表
schema_list:
- schema: rime_ice
# 中西文切换
#
# 【good_old_caps_lock】 CapsLock 切换到大写或切换中英。
# (macOS 偏好设置的优先级更高,如果勾选【使用大写锁定键切换“ABC”输入法】则始终会切换输入法)
#
# 切换中英:
# 不同的选项表示:打字打到一半时按下了 CapsLock、Shift、Control 后:
# commit_code 上屏原始的编码,然后切换到英文
# commit_text 上屏拼出的词句,然后切换到英文
# clear 清除未上屏内容,然后切换到英文
# inline_ascii 无输入时,切换中英;有输入时,切换到临时英文模式,按回车上屏后回到中文状态
# noop 屏蔽快捷键,不切换中英,但不要屏蔽 CapsLock
ascii_composer:
good_old_caps_lock: true # true | false
switch_key:
Caps_Lock: commit_code # commit_code | commit_text | clear
Shift_L: commit_code # commit_code | commit_text | inline_ascii | clear | noop
Shift_R: commit_code # commit_code | commit_text | inline_ascii | clear | noop
Control_L: noop # commit_code | commit_text | inline_ascii | clear | noop
Control_R: noop # commit_code | commit_text | inline_ascii | clear | noop
鼠须管有很多内置的主题,你可以直接将他们的名字填到下面配置的 color_schema 字段即可,我这里没有使用内置主题,而是从网络上找了两款接近 macOS 原生输入法的主题。
执行 vim ~/Library/Rime/squirrel.custom.yaml
,填入以下配置:
# Squirrel Patch settings
# encoding: utf-8
#
# 内置皮肤展示: https://github.com/NavisLab/rime-pifu
# 鼠须管配置指南: https://github.com/LEOYoon-Tsaw/Rime_collections/blob/master/鼠鬚管介面配置指南.md
# 鼠须管作者写的图形化的皮肤设计器: https://github.com/LEOYoon-Tsaw/Squirrel-Designer
patch:
style:
color_scheme: macos_light # 将皮肤名称输入在此处
color_scheme_dark: macos_dark # 暗色模式下的皮肤名称
# 皮肤列表
preset_color_schemes:
macos_light:
name: "MacOS 浅色/MacOS Light"
author: 小码哥
font_face: "PingFangSC" # 字体及大小
font_point: 16
label_font_face: "PingFangSC" # 序号字体及大小
label_font_point: 12
comment_font_face: "PingFangSC" # 注字体及大小
comment_font_point: 16
candidate_format: "%c\u2005%@\u2005" # 编号 %c 和候选词 %@ 前后的空间
candidate_list_layout: linear # 候选排布:层叠 stacked | 行 linear
text_orientation: horizontal # 行文向: 横 horizontal | 纵 vertical
inline_preedit: true # 拼音位于: 候选框 false | 行内 true
translucency: false # 磨砂: false | true
mutual_exclusive: false # 色不叠加: false | true
border_height: 1 # 外边框 高
border_width: 1 # 外边框 宽
corner_radius: 5 # 外边框 圆角半径
hilited_corner_radius: 5 # 选中框 圆角半径
surrounding_extra_expansion: 0 # 候选项背景相对大小?
shadow_size: 0 # 阴影大小
line_spacing: 5 # 行间距
base_offset: 0 # 字基高
alpha: 1 # 透明度,0~1
spacing: 10 # 拼音与候选项之间的距离 (inline_preedit: false)
color_space: srgb # 色彩空间: srgb | display_p3
back_color: 0xFFFFFF # 底色
hilited_candidate_back_color: 0xD75A00 # 选中底色
label_color: 0x999999 # 序号颜色
hilited_candidate_label_color: 0xFFFFFF # 选中序号颜色
candidate_text_color: 0x3c3c3c # 文字颜色
hilited_candidate_text_color: 0xFFFFFF # 选中文字颜色
comment_text_color: 0x999999 # 注颜色
hilited_comment_text_color: 0xFFFFFF # 选中注颜色
text_color: 0x424242 # 拼音颜色 (inline_preedit: false)
hilited_text_color: 0xFFFFFF # 选中拼音颜色 (inline_preedit: false)
candidate_back_color: 0xe9e9ea # 候选项底色
# preedit_back_color: # 拼音底色 (inline_preedit: false)
hilited_back_color: 0xD75A00 # 选中拼音底色 (inline_preedit: false)
border_color: 0xFFFFFF # 外边框颜色
macos_dark:
name: "MacOS 深色/MacOS Dark"
author: 小码哥
font_face: "PingFangSC" # 字体及大小
font_point: 16
label_font_face: "PingFangSC" # 序号字体及大小
label_font_point: 12
comment_font_face: "PingFangSC" # 注字体及大小
comment_font_point: 16
candidate_format: "%c\u2005%@\u2005" # 编号 %c 和候选词 %@ 前后的空间
candidate_list_layout: linear # 候选排布:层叠 stacked | 行 linear
text_orientation: horizontal # 行文向: 横 horizontal | 纵 vertical
inline_preedit: true # 拼音位于: 候选框 false | 行内 true
translucency: false # 磨砂: false | true
mutual_exclusive: false # 色不叠加: false | true
border_height: 1 # 外边框 高
border_width: 1 # 外边框 宽
corner_radius: 5 # 外边框 圆角半径
hilited_corner_radius: 5 # 选中框 圆角半径
surrounding_extra_expansion: 0 # 候选项背景相对大小?
shadow_size: 0 # 阴影大小
line_spacing: 5 # 行间距
base_offset: 0 # 字基高
alpha: 1 # 透明度,0~1
spacing: 10 # 拼音与候选项之间的距离 (inline_preedit: false)
color_space: srgb # 色彩空间: srgb | display_p3
back_color: 0x1f1e2d # 底色
hilited_candidate_back_color: 0xD75A00 # 选中底色
label_color: 0x999999 # 序号颜色
hilited_candidate_label_color: 0xFFFFFF # 选中序号颜色
candidate_text_color: 0xe9e9ea # 文字颜色
hilited_candidate_text_color: 0xFFFFFF # 选中文字颜色
comment_text_color: 0x999999 # 注颜色
hilited_comment_text_color: 0x999999 # 选中注颜色
text_color: 0x808080 # 拼音颜色 (inline_preedit: false)
hilited_text_color: 0xFFFFFF # 选中拼音颜色 (inline_preedit: false)
candidate_back_color: 0xe9e9ea # 候选项底色
# preedit_back_color: # 拼音底色 (inline_preedit: false)
hilited_back_color: 0xD75A00 # 选中拼音底色 (inline_preedit: false)
border_color: 0x050505 # 外边框颜色
wechat_light:
name: "微信浅色/Wechat Light"
author: 小码哥
font_face: "PingFangSC" # 字体及大小
font_point: 16
label_font_face: "PingFangSC" # 序号字体及大小
label_font_point: 13
comment_font_face: "PingFangSC" # 注字体及大小
comment_font_point: 16
candidate_format: "%c\u2005%@\u2005" # 编号 %c 和候选词 %@ 前后的空间
candidate_list_layout: linear # 候选排布:层叠 stacked | 行 linear
text_orientation: horizontal # 行文向: 横 horizontal | 纵 vertical
inline_preedit: true # 拼音位于: 候选框 false | 行内 true
translucency: false # 磨砂: false | true
mutual_exclusive: false # 色不叠加: false | true
border_height: 1 # 外边框 高
border_width: 1 # 外边框 宽
corner_radius: 5 # 外边框 圆角半径
hilited_corner_radius: 5 # 选中框 圆角半径
surrounding_extra_expansion: 0 # 候选项背景相对大小?
shadow_size: 0 # 阴影大小
line_spacing: 5 # 行间距
base_offset: 0 # 字基高
alpha: 1 # 透明度,0~1
spacing: 10 # 拼音与候选项之间的距离 (inline_preedit: false)
color_space: srgb # 色彩空间: srgb | display_p3
back_color: 0xFFFFFF # 底色
hilited_candidate_back_color: 0x79af22 # 选中底色
label_color: 0x999999 # 序号颜色
hilited_candidate_label_color: 0xFFFFFF # 选中序号颜色
candidate_text_color: 0x3c3c3c # 文字颜色
hilited_candidate_text_color: 0xFFFFFF # 选中文字颜色
comment_text_color: 0x999999 # 注颜色
hilited_comment_text_color: 0x999999 # 选中注颜色
text_color: 0x424242 # 拼音颜色 (inline_preedit: false)
hilited_text_color: 0x999999 # 选中拼音颜色 (inline_preedit: false)
candidate_back_color: 0xe9e9ea # 候选项底色
# preedit_back_color: # 拼音底色 (inline_preedit: false)
hilited_back_color: 0x79af22 # 选中拼音底色 (inline_preedit: false)
border_color: 0xFFFFFF # 外边框颜色
wechat_dark:
name: "微信深色/Wechat Dark"
author: 小码哥
font_face: "PingFangSC" # 字体及大小
font_point: 16
label_font_face: "PingFangSC" # 序号字体及大小
label_font_point: 13
comment_font_face: "PingFangSC" # 注字体及大小
comment_font_point: 16
candidate_format: "%c\u2005%@\u2005" # 编号 %c 和候选词 %@ 前后的空间
candidate_list_layout: linear # 候选排布:层叠 stacked | 行 linear
text_orientation: horizontal # 行文向: 横 horizontal | 纵 vertical
inline_preedit: true # 拼音位于: 候选框 false | 行内 true
translucency: false # 磨砂: false | true
mutual_exclusive: false # 色不叠加: false | true
border_height: 1 # 外边框 高
border_width: 1 # 外边框 宽
corner_radius: 5 # 外边框 圆角半径
hilited_corner_radius: 5 # 选中框 圆角半径
surrounding_extra_expansion: 0 # 候选项背景相对大小?
shadow_size: 0 # 阴影大小
line_spacing: 5 # 行间距
base_offset: 0 # 字基高
alpha: 1 # 透明度,0~1
spacing: 10 # 拼音与候选项之间的距离 (inline_preedit: false)
color_space: srgb # 色彩空间: srgb | display_p3
back_color: 0x151515 # 底色
hilited_candidate_back_color: 0x79af22 # 选中底色
label_color: 0x999999 # 序号颜色
hilited_candidate_label_color: 0xFFFFFF # 选中序号颜色
candidate_text_color: 0xbbbbbb # 文字颜色
hilited_candidate_text_color: 0xFFFFFF # 选中文字颜色
comment_text_color: 0x999999 # 注颜色
hilited_comment_text_color: 0xFFFFFF # 选中注颜色
text_color: 0xbbbbbb # 拼音颜色 (inline_preedit: false)
hilited_text_color: 0x999999 # 选中拼音颜色 (inline_preedit: false)
candidate_back_color: 0xbbbbbb # 候选项底色
# preedit_back_color: # 拼音底色 (inline_preedit: false)
hilited_back_color: 0x79af22 # 选中拼音底色 (inline_preedit: false)
border_color: 0x292929 # 外边框颜色
拷贝完之后,右上角输入法切换到 Squirrel 点击 Deploy 让配置生效。
上面我用的配置文件都是 *.custom.yaml 结尾,这种模式称之为打补丁模式,主要是为了避免修改原配置文件,这样以后输入法配置升级就很容易了。
使用鼠须管自带的用户数据同步功能配合 iCloud 之类的网盘,可以轻松将你的输入法配置和词库跨设备同步,对于多台设备,鼠须管会自动合并用户词库。
vim ~/Library/Rime/installation.yaml
# 本机的 ID 标志,默认是一串 UUID。生成的文件夹是这个名字,可以改成更好识别的名称
installation_id: "mbp16"
# 同步的路径,默认如没有设置此属性,则是在当前配置目录的 `sync/` 文件夹
sync_dir: "/Users/YOUR_NAME/Library/Mobile Documents/com~apple~CloudDocs/RimeSync"
Sync user data
即可完成数据同步,之后改动配置后需要再次重复此操作。目前官方没有提供自动触发同步的功能,因为在同步用户数据期间,输入法会处于不可用状态。RIME 的同步功能是 配置单向同步 + 用户词库双向同步,举个例子:A 设备开启同步功能后,B 设备并不会自动获得相同的配置,这两个设备只会自动合并用户词库(也就是你的自造词)。
如果你觉得经常手动同步用户数据比较麻烦,也可以配合 sleepwatcher 在电脑休眠和唤醒的时候自动触发同步,具体做法如下:
brew install sleepwatcher
touch ~/.sleep
chmod +x ~/.sleep
编辑 ~/.sleep 文件,填入以下内容:
#!/usr/bin/env bash
# 触发鼠须管同步用户数据
/Library/Input\ Methods/Squirrel.app/Contents/MacOS/Squirrel --sync
配置好之后,重启 sleepwatcher
brew services restart sleepwatcher
这时候可以休眠电脑等10秒再唤醒,检查 sync_dir 里面的 rime_ice.userdb.txt 文件修改时间是否变了。如果不生效,请检查 ~/.sleep 脚本是否正确,可以手动执行看下效果,以及重启 sleepwatcher.
上面的 ~/.sleep 脚本是在电脑休眠之前执行,根据你的需要还可以定义一个 ~/.wakeup 脚本在唤醒后执行。
前面我们用的预设配置是 雾凇拼音 这个项目提供的,后续可以通过下面的命令更新配置以获得新功能和 Bugfix。
更新雾凇拼音:所有配置和词库(更新前建议先备份 ~/Library/Rime 目录,更新后所有非 .custom.yaml 结尾的配置文件会被覆盖)
# 先回到 plum 安装目录,如果你将 plum 安装在了其他目录,这里需要修改
cd ~/plum
bash rime-install iDvel/rime-ice:others/recipes/full
更新雾凇拼音:所有词库文件
cd ~/plum
bash rime-install iDvel/rime-ice:others/recipes/all_dicts
如果你用下来感觉挺好的没啥问题,那建议只更新词库就行了。
对于自定义的补丁配置文件,尤其是 *.custom.yaml 和自定义字典,建议将其备份到自己的 git 仓库保存,防止配置意外被覆盖或丢失。
RIME 有些很有用的使用技巧新手刚开始不仔细研究可能发现不了,这里我罗列一些非常好用的技巧。
打字久了很容易在不经意间产生一些错误的自造词,这时候如果不处理就很容易在后续的输入过程中继续使用这些错误的自造词。
要删除不想要的自造词,你可以使用方向键选中要删除的候选词,再按下 Fn+Shift+Delete 即可删除。
手册原文:只能夠從用戶詞典中刪除詞組。用於碼表中原有的詞組時,只會取消其調頻效果。
当你打了一段话,发现前面的拼音打错了,或者想单独修改前面的某个字,这时候不用狂按退格键,你可以直接按 Tab 键或 Shift + Tab 在拼音中前后切换光标到对应的拼音。
按照上面的步骤操作下来你已经入门了,这时候如果你想让 RIME 输入法更顺手更符合你的习惯偏好就需要了解进阶概念了。
如果你想调整一些自定义的设置,可以先查看以下两个文件,可以对整体配置有个概念:
cat ~/Library/Rime/default.yaml
这是 Rime 输入法引擎的默认配置文件,大部分跟输入有关的核心配置都在这里# Rime default settings
# encoding: utf-8
#
# 小狼毫似乎不支持 Control+Shift 开头的快捷键,可自行修改成别的。
# 鼠须管在 Sublime Text、Telegram 等个别软件中也无法使用 Control+Shift+数字 的快捷键,可暂时用方案选单切换。
config_version: '2023-03-03'
# 方案列表
schema_list:
- schema: rime_ice
- schema: double_pinyin
- schema: double_pinyin_flypy
# 菜单
menu:
page_size: 5 # 候选词个数
# alternative_select_labels: [ ①, ②, ③, ④, ⑤, ⑥, ⑦, ⑧, ⑨, ⑩ ] # 修改候选项标签
# alternative_select_keys: ASDFGHJKL # 如编码字符占用数字键,则需另设选字键
# 方案选单相关
switcher:
caption: 「方案选单」
hotkeys:
# - F4
# - Control+grave
# - Alt+grave
- Control+Shift+grave
save_options: # 开关记忆,从方案选单(而非快捷键)切换时会记住的选项,需要记忆的开关不能设定 reset
- ascii_punct
- traditionalization
- emoji
fold_options: true # 呼出时是否折叠,多方案时建议折叠 true ,一个方案建议展开 false
abbreviate_options: true # 折叠时是否缩写选项
option_list_separator: ' / ' # 折叠时的选项分隔符
# 中西文切换
#
# 【good_old_caps_lock】 CapsLock 切换到大写或切换中英。
# (macOS 偏好设置的优先级更高,如果勾选【使用大写锁定键切换“ABC”输入法】则始终会切换输入法)
#
# 切换中英:
# 不同的选项表示:打字打到一半时按下了 CapsLock、Shift、Control 后:
# commit_code 上屏原始的编码,然后切换到英文
# commit_text 上屏拼出的词句,然后切换到英文
# clear 清除未上屏内容,然后切换到英文
# inline_ascii 无输入时,切换中英;有输入时,切换到临时英文模式,按回车上屏后回到中文状态
# noop 屏蔽快捷键,不切换中英,但不要屏蔽 CapsLock
ascii_composer:
good_old_caps_lock: true # true | false
switch_key:
Caps_Lock: clear # commit_code | commit_text | clear
Shift_L: noop # commit_code | commit_text | inline_ascii | clear | noop
Shift_R: noop # commit_code | commit_text | inline_ascii | clear | noop
Control_L: noop # commit_code | commit_text | inline_ascii | clear | noop
Control_R: noop # commit_code | commit_text | inline_ascii | clear | noop
# punctuator 和 recognizer 由具体方案指定了
punctuator:
full_shape:
half_shape:
symbols:
recognizer:
patterns:
# 快捷键
key_binder:
# 以词定字(上屏当前词句的第一个或最后一个字)
select_first_character:
select_last_character: "grave"
bindings:
# Tab/Shift+Tab 切换光标至下/上一个拼音
- { when: composing, accept: Shift+Tab, send: Shift+Left }
- { when: composing, accept: Tab, send: Shift+Right }
# Tab/Shift+Tab 翻页
# - { when: has_menu, accept: Shift+Tab, send: Page_Up }
# - { when: has_menu, accept: Tab, send: Page_Down }
# 翻页 - =
- { when: has_menu, accept: minus, send: Page_Up }
- { when: has_menu, accept: equal, send: Page_Down }
# 翻页 , .
# 需要额外注释掉方案中 recognizer/patterns 下的 url_2 选项(这个会覆盖掉句号的行为)
# - { when: paging, accept: comma, send: Page_Up }
# - { when: has_menu, accept: period, send: Page_Down }
# 翻页 [ ]
# - { when: paging, accept: bracketleft, send: Page_Up }
# - { when: has_menu, accept: bracketright, send: Page_Down }
# numbered_mode_switch:
# - { when: always, accept: Control+Shift+1, select: .next } # 在最近的两个方案之间切换
# - { when: always, accept: Control+Shift+2, toggle: ascii_mode } # 切换中英
- { when: always, accept: Control+Shift+3, toggle: ascii_punct } # 切换中英标点
- { when: always, accept: "Control+Shift+4", toggle: traditionalization } # 切换简繁
# - { when: always, accept: Control+Shift+5, toggle: full_shape } # 切换全半角
# key_binder 按键速查 https://github.com/LEOYoon-Tsaw/Rime_collections/blob/master/Rime_description.md
cat ~/Library/Rime/squirrel.yaml
这是鼠须管输入法的配置,包含主题配色之类的设置# Squirrel settings
# encoding: utf-8
#
# 内置皮肤展示: https://github.com/NavisLab/rime-pifu
# 鼠须管配置指南: https://github.com/LEOYoon-Tsaw/Rime_collections/blob/master/鼠鬚管介面配置指南.md
# 鼠须管作者写的图形化的皮肤设计器: https://github.com/LEOYoon-Tsaw/Squirrel-Designer
config_version: '2023-02-27'
# options: last | default | _custom_
# last: the last used latin keyboard layout
# default: US (ABC) keyboard layout
# _custom_: keyboard layout of your choice, e.g. 'com.apple.keylayout.USExtended' or simply 'USExtended'
keyboard_layout: default
# for veteran chord-typist
chord_duration: 0.1 # seconds
# options: always | never | appropriate
show_notifications_when: appropriate
# ascii_mode、inline、no_inline、vim_mode 等等设定,可参考 /Library/Input Methods/Squirrel.app/Contents/SharedSupport/squirrel.yaml
app_options:
# com.apple.Spotlight:
# ascii_mode: true # 开启默认英文
# com.microsoft.VSCode:
# ascii_mode: false # 关闭默认英文
style:
# 选择皮肤,亮色与暗色主题
color_scheme: purity_of_form_custom
color_scheme_dark: purity_of_form_custom
# 预设选项:(可被皮肤覆盖;如果皮肤没写,则默认使用这些属性。)
text_orientation: horizontal # horizontal | vertical
inline_preedit: true
corner_radius: 10
hilited_corner_radius: 0
border_height: 0
border_width: 0
line_spacing: 5
spacing: 10
#candidate_format: '%c. %@'
#base_offset: 6
font_face: 'Lucida Grande'
font_point: 21
#label_font_face: 'Lucida Grande'
label_font_point: 18
#comment_font_face: 'Lucida Grande'
comment_font_point: 18
# 皮肤列表
preset_color_schemes:
# 对 purity_of_form 略微调整颜色,让色彩更柔和点,补全其他选项和注释
purity_of_form_custom:
name: "純粹的形式/Purity of Form Custom"
author: 雨過之後、佛振
font_face: "" # 字体及大小
font_point: 18
label_font_face: "Helvetica" # 序号字体及大小
label_font_point: 12
comment_font_face: "Helvetica" # 注字体及大小
comment_font_point: 16
candidate_list_layout: stacked # 候选排布:层叠 stacked | 行 linear
text_orientation: horizontal # 行文向: 横 horizontal | 纵 vertical
inline_preedit: true # 拼音位于: 候选框 false | 行内 true
translucency: false # 磨砂: false | true
mutual_exclusive: false # 色不叠加: false | true
border_height: 0 # 外边框 高
border_width: 0 # 外边框 宽
corner_radius: 10 # 外边框 圆角半径
hilited_corner_radius: 0 # 选中框 圆角半径
surrounding_extra_expansion: 0 # 候选项背景相对大小?
shadow_size: 0 # 阴影大小
line_spacing: 5 # 行间距
base_offset: 0 # 字基高
alpha: 1 # 透明度,0~1
spacing: 10 # 拼音与候选项之间的距离 (inline_preedit: false)
color_space: srgb # 色彩空间: srgb | display_p3
back_color: 0x545554 # 底色
hilited_candidate_back_color: 0xE3E3E3 # 选中底色
label_color: 0xBBBBBB # 序号颜色
hilited_candidate_label_color: 0x4C4C4C # 选中序号颜色
candidate_text_color: 0xEEEEEE # 文字颜色
hilited_candidate_text_color: 0x000000 # 选中文字颜色
comment_text_color: 0x808080 # 注颜色
hilited_comment_text_color: 0x808080 # 选中注颜色
text_color: 0x808080 # 拼音颜色 (inline_preedit: false)
hilited_text_color: 0xEEEEEE # 选中拼音颜色 (inline_preedit: false)
# candidate_back_color: # 候选项底色
# preedit_back_color: # 拼音底色 (inline_preedit: false)
# hilited_back_color: # 选中拼音底色 (inline_preedit: false)
# border_color: # 外边框颜色
碰到你想修改的配置项,有两种修改方案:
修改配置文件后记得重新 Deploy。
cat ~/Library/Rime/custom_phrase.txt
查看现有短语和配置说明对于拼音输入法来说,不管是全拼还是双拼,都面临一个问题:重码。相同的拼音对应多个发音类似的词语,如果第一屏候选词没出现用户想要的词,就会严重影响输入效率。
一款输入法好不好用,在很大程度上是看这款输入法的字典好不好用,字典不是越大越多越好,而是要契合用户的使用场景,越大的字典重码率通常会越高,越容易让用户感觉首屏候选词经常出现不了自己想要的词。
根据自己的日常使用场景来挂载对应的词库,并在平时逐步定制积累自定义词库是一个不错的选择。
本文使用的雾凇拼音已经默认做了很多输入优化和字典优化,如果你使用一段时间后想实现更好的打字效果,制作你的自定义字典是个不错的选择(当然你多次重复输入一个词语也会自动造词的)
举个例子,一些大公司喜欢让员工起花名,随着时间推移员工人数飞速增长,起两个字的中文花名就是个很困难的事情,所以经常能看到许多新员工的花名奇奇怪怪,生僻字和谐音很常见。这时候你用拼音输入法打同事的花名就非常麻烦和难受。针对这种情况就可以用自定义字典很好的解决,后续输入同事的花名效率就会非常高。
要制作自定义字典,请先切换到 Rime 的用户配置目录:cd ~/Library/Rime
先查看雾凇拼音的默认字典配置:cat rime_ice.dict.yaml
# Rime dictionary
# encoding: utf-8
---
name: rime_ice
version: "2023-03-04"
import_tables:
- cn_dicts/8105 # 字表
# - cn_dicts/41448 # 大字表(按需启用)
- cn_dicts/base # 基础词库
- cn_dicts/sogou # 搜狗流行词
- cn_dicts/ext # 扩展词库
- cn_dicts/tencent # 腾讯词向量(大词库,部署时间较长)
- cn_dicts/others # 一些杂项
# 建议把扩展词库放到下面,有重复词条时,最上面的权重生效
# 下面两个是我自己加的自定义字典
- cn_dicts/names # 自定义人名词库
- en_dicts/my_en_ext # 扩充英文词语
...
编辑自定义字典:vim cn_dicts/names.dict.yaml
,这里的 names.dict.yaml 要改成你自己的字典名称。
内容是这样的:
---
name: names
version: "1"
sort: by_weight
...
维恩 wei en 100
步鲁斯 bu lu si 100
这里需要格外注意字典内容的缩进,正确的示例如下,用 Tab 分割不同的列,用空格分割拼音:
拼音 pin yin 1234
拼音<Tab>pin<Space>yin<Tab>1234
最好不要直接复制这段配置(制表符被转换为空格了),RIME 解析字典要依赖制表符识别不同的列,在编辑字典文件的时候记得先将编辑器缩进模式设置为 Tab 模式,建议先查看雾凇拼音的文档:编写词库,如果字典里的词没出现在首屏,可以增加权重,例如:1000000,默认建议先设置 100 就可以了。
通过几分钟时间你可以快速配置出一个好用、安全、隐私、轻量的输入法,它可以充分按照你的习惯和需求去定制,而且 RIME 有一个不错的用户手册可供你参考。如果你在意以上这些特点,那 RIME 可能是目前最好的选择。虽然它并不是完美的,也有一些小毛病,就看你怎么取舍了。
如果你想在 iOS 上也使用和 Mac 相同的词库,可以试一试 iOS 上的一款 Rime 实现:仓输入法。我自己实测下来直接将 Mac 端 Rime 配置导入到 iOS 端的仓输入法可以正常使用,不过得手动同步有些低效。
要获得最佳阅读体验,请访问原文 https://baiyun.me/rime-simple-tutorial]]>
我们可以使用 Git 的 includeIf 功能为不同的 Git 仓库设置不同的电子邮件地址,并实现自动化。这个功能可以根据 Git 仓库的路径自动加载指定的配置文件。
以下是具体步骤:
首先创建不同的 Git 配置文件。例如,可以在你的家目录中创建两个文件 .gitconfig-personal
和 .gitconfig-work
,并分别为个人项目和工作项目设置不同的用户配置。可以使用以下命令创建这些文件:
touch ~/.gitconfig-personal
touch ~/.gitconfig-work
使用 vim 编辑以上两个 Git 配置文件:
[user]
name = Your Name
email = your.email@example.com
将 "Your Name" 和 "your.email@example.com" 替换为你要用的名称和邮箱地址。
最后编辑 ~/.gitconfig
文件,将以下代码添加到文件的末尾:
[includeIf "gitdir:/path/to/personal/"]
path = ~/.gitconfig-personal
[includeIf "gitdir:/path/to/work/"]
path = ~/.gitconfig-work
在上面的代码中,/path/to/personal/
和 /path/to/work/
分别是个人项目和工作项目的所处的目录,可以根据你的实际情况替换。
这些代码的作用是在匹配到 Git 仓库的路径时自动加载相应的配置文件。例如,如果进入个人项目的 Git 仓库目录,Git 将自动加载 ~/.gitconfig-personal 文件,并使用其中定义的用户名和邮箱地址。
以上,你已经为不同的 Git 仓库设置了不同的邮箱地址,并实现了配置自动化生效。
最后从安全角度推荐使用 ssh 协议和 git 仓库交互。
要获得最佳阅读体验,请访问原文 https://baiyun.me/set-different-emails-for-git-repos]]>
钱包是进入加密世界的入口,现在市面上的钱包软件玲琅满目,其中不乏有些鸡鸣狗盗之徒。所以对于新手来说选择一个安全可靠且趁手的钱包是至关重要的。
关于如何选择,我建议优先考虑以下几个点:
下面是我个人的一些推荐,排名越靠前越推荐新手使用
注意下载来源,尤其是安卓和 PC 用户,下载钱包我都推荐用 https://google.com 搜索钱包名称,通常第一个就是官网(注意不要点广告)。再次强调:安卓用户非常不推荐在国产应用商店里下载钱包。
可以,主流钱包软件都遵循 BIP 协议规范,所以助记词在这些钱包之间是通用的。
要获得最佳阅读体验,请访问原文 https://baiyun.me/how-to-choose-a-crypto-wallet]]>
在圈内作为用户体验了一段时间后,我的理解是这样的,本质上 Web3 是为了解决 Web2 时代的一些痛点,随着区块链技术和以太坊为代表的公链井喷式发展,Web3 就有了实际落地的能力。
首先看最重要的三个要素
Web3 应用由世界各地大量的节点(矿工)来同时提供服务,每个节点都可以对用户提供相同的服务。拿以太坊来举例,整个以太坊就是一台超级分布式的计算机和数据库,它可以执行你的代码和计算逻辑,保存你的数据,并保证数据正确没有篡改。
上图可以看到,中心化服务是通过少数的中心节点来给所有用户提供服务,这些中心服务器一旦停机或故障就会影响几乎所有的用户。
你的账号产生的数据永远属于你,例如你在以太坊上 Mint 一个 NFT 或者保存一篇文章,那么这些数据的所有权完全归属在你的账号下,当然这些数据产生的收益也归你。
数据一旦被矿工打包则几乎不可篡改和删除。
后续我的博客也计划将公开文章在区块链上存档一份,届时我会更新文章。
Web3 毫无疑问是有巨大前景的,就目前而言,原生 Web3 应用对普通用户的门槛还是太高了,这需要整个行业从钱包易用性到上层 DApp 应用,再到公链共同探索进步。目前 NFT 已经有过一轮引爆点,等到下轮牛市周期开启,相信会有新的引爆点出现。
想了解更多推荐看一遍下面的内容:
要获得最佳阅读体验,请访问原文 https://baiyun.me/what-is-web3]]>
例如:
SameSite=None; Secure
的 Cookie 不会被发送。进而导致基于 Cookie 的登录认证等功能失效window.onerror
的 callback 会清一色收到 Script error
,严重影响开发人员定位问题
如果你不想在上网冲浪时遇到莫名奇妙的问题(例如登录功能失效、重定向死循环),建议在 Safari 设置中关闭上图中框住的选项。
要获得最佳阅读体验,请访问原文 https://baiyun.me/do-not-enable-prevent-cross-site-tracing-on-safari]]>
对于遇到这种情况的用户来说,我的建议是不要让它们浪费你的生命,评估下这款应用是否有不可代替性,尽量直接切换到其他同类应用,并在 App Store 给个建议或差评然后将其卸载。截止目前 T3 出行和民生银行、上海银行等客户端我已经直接卸载了。
如果因为各种原因你暂时还得忍受这些应用,一个解决办法是开启 Surge 的 TUN Only 兼容模式:
[General]
compatibility-mode = 3
除了用配置文件开启外,你也可以直接在 Surge 的更多设置 => 兼容模式 里选择 TUN Only
这种方法有很多副作用,按照Surge 文档上的说法,开启 TUN Only 模式后会导致 HTTP 相关的高级功能失效,且性能会稍有下降。
另外根据我的实际体验,开启 TUN Only 之后会导致代理链不支持 HTTP3 UDP 流量,现在很多网站都默认启用了 HTTP3,而 HTTP3 是用 UDP 协议承载的,在国内网络环境下 UDP 流量直接发往目标代理服务器会导致网络性能极差,简而言之,一旦开启 TUN Only 你的代理链在很多情况下是无法生效的。
针对上述情况,可以使用 Surge 提供的模块功能以及 HTTP API 搭配 iOS 的「快捷指令」程序实现自动化开启和关闭 TUN Only 兼容模式,遗憾的是,这种方法也是有缺点的,因为快捷指令是在打开 APP 之后才会触发 Surge API 调用,大部分 APP 在打开的一瞬间就会触发网络请求,这时候还是会检测到代理报错。
如果你想试一试这种方法,可以参考下述操作方法:
首先添加以下配置到你的 Surge 配置文件开启 Surge 的 HTTP API 功能。
[General]
http-api = 你的密码@127.0.0.1:7170
打开 Surge 安装以下模块,你可以将以下代码上传到合适的地方再用 URL 导入,也可以直接新建本地模块。
#!name=TUN Only
#!desc=开启 Tun Only 兼容模式,解决某些 APP 不能正常连接的问题
#!system=ios
[General]
compatibility-mode = 3
一切准备就绪后,打开手机上的快捷指令 APP 开始创建自动化流程
选好了下一步
到这里就实现了当打开 xxx APP 的时候自动开启 Surge 的 TUN Only 模式。如果要实现关闭 APP 时自动关闭 TUN Only 模式,你可以按照上述步骤再来一遍。
最近发现有用户在 Github 开源了一个用于优化 Surge 和某些应用兼容性的模块,可以给我们参考:关于通用设置增强模块功能
检测到开代理就不让用是一种非常恶心用户的做法,这种行为并不能实质性提升应用的安全性,只能说设计这套逻辑的相关 PD 和开发者都非常不专业。所以遇到这种表现的 APP,我更建议你通过 APP 的投诉入口或者客服电话进行投诉和建议。
要获得最佳阅读体验,请访问原文 https://baiyun.me/surge-with-china-apps]]>
推测是群发 Spam 的软件大多依赖用户名才能正常工作,所以当你没有用户名的时候大部分这类软件就无法给你发私信了。
要获得最佳阅读体验,请访问原文 https://baiyun.me/block-spam-on-telegram]]>
要获得最佳阅读体验,请访问原文 https://baiyun.me/everyone-should-read-1984-and-animal-farm]]>
下图是效果图,可以看到左边头像都变成对应的 LOGO 了
项目地址:https://github.com/metowolf/vCards
下载地址:https://github.com/metowolf/vCards/releases
为了避免导入重复的联系人,推荐按照我的方法选择数据源:打开上面的下载地址下载 archive.zip 并解压,依次打开文件夹 archive => 汇总。找到 全部.vcf 将它拖到 macOS 的联系人应用里,iOS 可以参考原文档的说明。
要获得最佳阅读体验,请访问原文 https://baiyun.me/ios-vcard]]>
实际上正确购买比特币的姿势是注册正规加密货币交易所账号,并在交易所完成 KYC。之后你的交易才会受到保障,且整体交易流程更加完善和方便。
目前很多交易所已经不支持中国大陆的身份证注册了,下面是我自己实际测试过可以用中国身份证以及护照注册,且比较靠谱的一些交易所:
对于新手我推荐你先注册币安,这是目前世界上最大的加密货币交易所,且对中国用户非常友好。你可以根据国内注册币安交易所账号并通过 KYC 教程来注册。
kraken.com 是一家接受美国、加拿大和英国监管的交易所,可以直接用海外银行入金,网站和 App 的交互体验设计的非常优秀,对新手非常友好。如果你有海外银行账号,用 kraken 出入金是比较方便的。对于轻度用户来说我非常推荐你使用 Kraken App。
okx.com 对国内用户来说整体和币安类似,它近几年整体进步非常迅速,App 好用,某些功能例如子账户要比币安好很多。
以上介绍了目前在国内购买加密货币的正确方法,可以帮你打开新世界的大门。最后提醒一点,投资存在风险。在这个领域中有一句经常被提及的话是 DYOR(do your own research),意思是说要自己多做研究、多思考。对于拥有大量资金的人来说,不建议把所有鸡蛋放在一个篮子里,最好分散存储,毕竟就算像当年世界第二大交易所 FTX 也能够在一夜之间倒闭。
要获得最佳阅读体验,请访问原文 https://baiyun.me/buy-bitcoin-in-china]]>
本文适合于专业和普通用户
让用户实际感知到你的网站使用起来非常快是很重要的。性能优化的方法和细节非常多,且不同的网站形态需要对症下药。本文不会一个个细节讲下来,而是会授人以渔,告诉你方法论,让你自己能发现性能问题,知道如何发现和解决性能问题。
不管是专业开发者还是普通用户,我都推荐你使用 Google 提供的 PageSpeed Insights 来直观的发现性能问题以及对应的解决方案。
输入你的网站地址就可以看到在移动端和电脑端的性能分数以及对应的优化建议
点击对应的结果展开详细信息就可以看到问题的详细描述,以及每一项后面都有一个「了解更多」可以让你更详细的了解这个问题的原因和解决方案。
可以把 PageSpeed Insights 这个工具当做你优化网站性能的顶级良师益友。
loading="lazy"
属性可以原生实现图片懒加载效果content-visibility: hidden;
属性可以优化浏览器渲染性能
要获得最佳阅读体验,请访问原文 https://baiyun.me/web-performance]]>
对于大多数人来说最靠谱的方法就是定投比特币和以太坊,这两个或许短时间内不会再翻十倍百倍,但也不会太差。
最重要的是不要把吃饭的钱投资进去,时刻要保持充足的现金流,才能在熊市中获得更高回报率。
不要被各种所谓的蓝筹币分散资金,不要 FOMO 各种山寨币,将有限的子弹聚集在比特币和以太坊上。其他的都是土狗。
持有时间线要拉长,不要关注这中间的波动,老老实实等到下一轮高峰再逐步变现。
要获得最佳阅读体验,请访问原文 https://baiyun.me/about-cryptocurrency-investment]]>
可以看到,只要严格按照本文的方法是可以立马收到验证码的:
最后把验证码填上去,点下一步,接下来会让你填写密码以及头像之类的信息,一路点下去就注册成功了。
要获得最佳阅读体验,请访问原文 https://baiyun.me/how-to-sign-up-twitter]]>
按照本文的方法可以实现用中国身份证成功注册币安帐户并正常通过 KYC 验证。用中国身份证 KYC 的好处是可以正常使用人民币进行 C2C 出入金,这在当前出入金越来越难的坏境下尤其可贵。同时为了账户安全,请使用自己本人的身份证进行 KYC,虚假KYC会引发币安风控导致封号。
注册流程推荐用电脑操作
邮箱和密码填好后点注册按钮,之后你的邮箱会收到验证码。
输入邮箱收到的验证码,点提交按钮完成注册:
通过邮箱验证后,接下来的关键是完成 KYC 才能正常使用币安的功能。
点击下图的 去验证 按钮进入到 KYC 验证流程:
如果你没看到上面的 去认证 引导按钮,说明币安调整了页面 UI,你可以直接访问币安的身份认证页面按照页面提示完成后续 KYC 认证。
进入到 KYC 验证流程后,首先要将居住地改为中国。
这里点击 继续 之后就不能自己手动修改居住地了,所以不要选错了。
如果你是直接访问币安身份认证页面,那么看到的是类似下面的样子,同样得将居住地改为中国:
前面的步骤都做好之后,开始填写用于 KYC 的个人信息,国籍确认选了中国,姓名一定要和身份证或护照保持一致。
这里点了继续之后,姓名就不能再手动更改了。如果你填错了需要联系币安客服修改。
姓名填好后,点击继续,根据页面提示选择证件类型,然后用电脑摄像头拍摄证件照片提交即可。
证件照片上传之后,币安会要求你用电脑的摄像头或币安手机客户端自拍一张人脸照片,用来检查是否为本人 KYC。
如果你的电脑没有摄像头,币安网页上会提示你用手机APP完成人脸认证。可以从币安官方网站下载手机 APP:https://www.binance.com/zh-CN/download
切勿从不明来源的地方下载,否则容易造成安全风险。
对于 iOS 用户来说,你需要先去注册一个非中国大陆区域的 Apple 账号,然后用这个海外区的 Apple 账号登录 App Store 才能下载币安 APP。不会注册或者嫌麻烦可以直接淘宝买一个,用关键字 "海外号" 搜索,私聊卖家你需要海外苹果账号即可。
动手能力强的可以参考这个视频自己注册海外 Apple 账号。
以上所有 KYC 资料填写正确提交后,如果资料验证没有问题,大部分情况几个小时就能收到验证通过的邮件,然后你就可以愉快的在币安上进行人民币出入金并交易了。
注意:如果你很快收到了验证不通过的邮件,请检查上传身份证的时候正反面是不是选错了,这个失败原因很常见,调整下顺序重新上传身份证照片就行。
如果有其他问题欢迎发邮件给我:hi@baiyun.me
要获得最佳阅读体验,请访问原文 https://baiyun.me/how-to-sign-up-binance-account-and-pass-kyc]]>
使用 MetaMask 点击 N 次 创建账户 按钮即可。经过我的实际测试,使用同一组助记词创建出来的钱包账户是可预测和固定的。也就是说使用同一组助记词,在 A 电脑的 MetaMask 创建的第 N 个账户一定和在 B 电脑上创建的第 N 个账户地址是一样的。
在 MetaMask 使用助记词导入账户,MetaMask 默认只会恢复顺序靠前且有余额的账户,后面的账户就要自己点 创建帐户 按钮手动恢复了。
一套助记词生成多个以太坊账户虽然比较方便,但是一旦这套助记词泄漏就相当于所以派生账户都泄漏了。
根据你的需求和使用场景,要自己权衡利弊。
要获得最佳阅读体验,请访问原文 https://baiyun.me/create-multiple-wallet-accounts-using-mnemonic-phrase]]>
没有 Apple Watch 的时候手机静音后放桌子上经常漏接电话,一些关键通知也无法及时看到。有了 Apple Watch 之后所有你想及时接收的通知都不会漏掉。
默认情况下,iPhone 上所有应用的通知权限都会镜像到 Apple Watch,我在 Watch 应用里关闭了大多数应用的通知权限,确保只有关键信息的通知才会触发 Apple Watch 通知。
在一些行情起伏关键的时期,可以在勿扰模式和睡眠模式里将交易相关应用加入到允许通知的应用名单里。当行情触发关键点位的时候你的 Apple Watch 就能及时通知你,同时不会吵到其他人。配合 TraddingView 的警报功能使用效果更好。
在佩戴 Apple Watch 期间,其他苹果设备只要点亮屏幕就可以自动被 Apple Watch 解锁,免去了输密码或者其他解锁操作。
长期使用能让你对自己的基础身体数据有个基本了解,尤其是心率和血氧异常。
我目前只有两条运动表带,用起来省心省力,其中一条是 Nike 的洞洞表带,在夏天佩戴手腕可以不那么闷。
要获得最佳阅读体验,请访问原文 https://baiyun.me/uses-of-apple-watch]]>
单纯连接钱包这个动作并不会导致你的资产被盗,是否会被盗主要取决于你在 MetaMask 上批准了哪些操作,同意连接钱包后 MetaMask 以及其他正规的钱包并不会给网站分享你的密钥,所有敏感操作 MetaMask 都会弹框向你询问是否执行。作为用户我们需要正确识别 MetaMask 上不同的操作类型它潜在的风险。
本文用 MetaMask 举例,但这些道理在其他钱包软件上也是相通和适用的。
以下的操作类型相对是安全可预期的,作为用户我们能明确知道自己做了什么事情。所有操作细节 MetaMask 都明确告诉你了,没啥猫腻。
真正高风险的是一些涉及合约批准或者调用的操作,这些操作对于不懂合约代码的用户来说很难去验证对方合约内部做了什么操作,对于一些恶意合约,可能 MetaMask 上显示的交易费用只有 Gas 费,但是你一旦批准了,对方就悄悄获取了转移你资产的权限。
下面是一些常见的高风险操作案例,讲完这些案例之后,本文会教你一个简单的办法杜绝大多数诈骗手法。
例如上图中当你点了确认后就授权了这个合约转移你的 WETH 的权限。之后这个合约可以在任意时间转走的你的 WETH。所以遇到 MetaMask 提醒你批准某个授权的时候一定要看清楚合约和网站是否正规。
这种 TOKEN 批准一般在使用 1inch 和 SushiSwap 这类去中心化交易所的时候都会出现,这种是正常的。如果其他类型的网站需要这个权限你就要留心了。
这是 OpenSea 在上架 NFT 之前要求你批准的操作,批准后 OpenSea 就有了对你的 NFT 的转移权限。当然 OpenSea 是正规应用,所以理论上你批准这个敏感操作是安全的。
这是一个 NFT 网站的 Mint 操作,Mint 操作在 NFT 领域是很常见的行为,但是同样不可掉以轻心。如果对方没有上传源代码,理论上依旧可以把恶意操作包装成普通 Mint 操作。所以对于不懂合约代码的普通用户来说,你需要确保 Mint 操作是在对方官网上进行的。避免被钓鱼网站坑了。
当然,上图中举例的这个合约是安全的,MetaMask 显示它的合约代码已经在 Etherscan 验证, 说明它的源代码已经上传到 Etherscan 网站,我们可以直接点击上图框住的 "Etherscan" 去查看它的代码实现
对于没有上传代码的合约,你就要非常谨慎了。
上图就是一个真实的利用 NFT Mint 来盗取别人资产的例子,可以看到点击 mint 按钮后,弹出的钱包对话框里显示的方法是 SET APPROVAL FOR ALL
,一旦你点了同意,对方就可以转移走你的资产。
具体的骗子套路是这样的,骗子首先通过 API 接口知道了你的钱包里有哪些 NFT 和 Token,并拿到对应的合约地址,之后就会在你点击 mint 按钮时骗子会调用对应合约的 SET APPROVAL FOR ALL
方法,普通小白用户或者手快大意的用户可能很容易就点同意按钮,然后中招了。
setApprovalForAll 是个极度危险的操作,当你在对方网站上领空投或者免费 mint 时如果触发了这个操作一定要及时拒绝。
不懂代码的用户很难通过检查合约代码来判断操作是否安全,不过我们可以通过判断合约调用是否包含 address 类型的参数来杜绝大多数的危险操作。
在对方网站上 Mint 免费 NFT 或者领空投的时候一定要在 MetaMask 弹出的确认框里切换到「十六进制文件」标签,检查当前合约调用的参数是否包含上图中的 type: address
字样。如果是的话,你就要格外格外小心了。最好立即点拒绝,然后再找懂行的人去确认是否安全。
如果你手快不小心点了可以操作,立即去 https://revoke.cash/ 撤销所有可疑授权,如果你不知道哪些可疑,挨个 revoke 所有的即可。
如果有一天你突然发现自己的钱包莫名奇妙多了一些不知名的 NFT 和小币种,不要去动它,更不要去卖或者转账。因为转账操作会和对方合约发生交互,很容易就在你看不懂的情况下授权对方转走你其他资产的权限。
推荐的操作是直接将这些 NFT 和币种隐藏掉,如果一定要操作,请确保先了解清楚对方合约代码。
为了确保长期安全,我们需要避免将所有鸡蛋放在一个篮子里,好的习惯是,平时分多个钱包使用,具体可以参考如下:
对于 MetaMask 和 Coinbase Wallet 这种钱包来说,用户可以用一套助记词创建出多个账户,维护起来非常简单。比如下图中你只需要点击 创建账户 按钮就可以创建一个账户出来,以后用同一套助记词可以将这些账户恢复出来,不过 MetaMask 上用助记词恢复账户的时候默认只恢复 Account 1 也就是第一个账户,后面的这些账户需要你再点击 创建账户 按钮来恢复。
Coinbase Wallet 是默认就帮你创建好了 10个以太坊钱包帐户,这和你在 MetaMask 里面手动点击 创建帐户 按钮创建出来的是完全一样的。
下面这些工具可以查看你之前批准和授权过哪些合约,必要情况下你可以撤销这些授权,理论上授权越多,未来遇到安全问题的几率越大。
非常推荐,一键查询多条链上的数据,支持 Token 和 NFT
这个一次只能查看一条链的数据,手动切换网络才能查询其他链,不支持 NFT 的授权查询
每条链都有自己的浏览器,例如 etherscan 和 bscscan。这些浏览器基本都提供了授权检查功能。下面分别是 ETH、BSC、Polygon 这三条链的官方浏览器提供的授权检查工具。
对于上面没有列出来的公链,你可以用:公链名称 + token approval checker
作为关键字在 Google 搜索
这里用 https://bscscan.com/tokenapprovalchecker
举例,下图框住的这些就是授权批准过的合约,这些合约可以转走你账户里所有授权过的 token。
连接钱包并不可怕,真正需要谨慎的是一些看不太懂的批准或者合约调用操作。我们需要细心检查 MetaMask 确认弹框里显示的操作是否符合你的预期,对于看不懂的,你需要确保你打开的网站是从官方渠道获取的,而不是钓鱼网站。实在不放心的就用小号钱包操作。
要获得最佳阅读体验,请访问原文 https://baiyun.me/wallet-safety-guide]]>
brew install z
要获得最佳阅读体验,请访问原文 https://baiyun.me/frequently-used-apps-on-macos]]>
在 web2 时代,几乎所有的网站和应用都需要自行保存用户的账户信息,且每家的账号不通用,用户需要记住所有的这些账号信息,为了管理这些账号密码信息,业界甚至诞生了很多类似 1Password 这样的密码管理器帮助用户管理繁多的账号信息。即使后来大多数网站都接入了基于 OAuth2 的社交媒体账号登录功能,但本质上依然存在上述问题,且最重要的是这些账号和账号产生的数据"可以不属于"用户所有。
到了目前的 web3 时代就明显不一样了,如果你经常使用 web3 相关的产品,可能已经发现这些 web3 的网站和应用基本都不提供"注册"功能,且登录的时候都是通过连接加密钱包来完成的,用户不需要输入用户名密码。下图是一个典型的 web2 应用和 web3 应用的登录页面对比图。
现如今的加密钱包帐户主要是基于公私钥加密机制,用户的帐户地址是公开的,这相当于公钥,钱包可以使用用户的私钥对一串数据进行签名从而拿到一串签名数据,之后其他用户可以在没有你的私钥的情况下通过密码学方式验证某一串数据是否和指定的签名相匹配。如果匹配则表示该用户是帐户的持有者。
我们用 Ethers.js 做个简单的代码示例:
本文使用的 ethers 版本是 ^5
import { ethers } from 'ethers'
// 这里省略了 web3Modal 的配置相关代码
const provider = await web3Modal.connect()
await provider.enable() // 连接到钱包,这一步钱包会弹出对话框向用户询问是否连接到当前站点
// The "any" network will allow spontaneous network changes
const web3Provider = new ethers.providers.Web3Provider(provider, 'any')
// The MetaMask plugin also allows signing transactions to
// send ether and pay to change state within the blockchain.
// For this, you need the account signer...
const signer = web3Provider.getSigner()
const message = `请签名证明你是钱包账户的拥有者\n\nNonce\n${Date.now()}`
// signMessage 方法会让钱包弹出对话框询问用户是否同意签名,用户同意后我们就可以拿到签名
const signature = await signer.signMessage(message)
// 调用后端登录 API,把当前用户的钱包地址以及上面的 message 和 signature 传给后端
loginApi({ signature, message, address })
有了签名后,我们在服务端使用 verifyMessage 方法验证 message 是否和签名匹配,如果签名是匹配的,则 verifyMessage 方法返回的地址肯定会和用户的钱包地址一致(比较过程需要忽略大小写)
// ethers 可以在 Node.js 和浏览器环境运行
import { ethers } from 'ethers'
function loaginHandler(req, res, next) {
const { message, signature, address } = req.body
const recoveredAddress = ethers.utils.verifyMessage(message, signature)
if (recoveredAddress.toLowerCase() === address.toLoawerCase()) {
res.json({ message: '登录成功了' })
}
}
以上代码我们通过 Ethers.js 提供的 signer.signMessage()
和 ethers.utils.verifyMessage()
这两个方法验证了用户是否为某个地址的持有者。对于验证通过的用户,我们就可以用 JWT Token 之类的方法给用户颁发鉴权信息,从而实现登录功能。
更详细的流程如下:
signMessage(message)
方法把 message 传进去让用户签名verifyMessage(message, signature)
方法检查此方法返回的地址是否和用户提供的一致在生产环境下调用 signMessage 时你应该在 message 中包含一个一次性的 Nonce 进去,并在服务端检查 Nonce 是否被使用,从而避免重放攻击。
当你比较两个钱包地址是否相等或者将钱包地址保存在数据库之前,不要忘记将地址全部转为小写,避免大小写不一致导致意外情况。
MetaMask 是目前用户最多的钱包软件,不过像 Coinbase Wallet、Trust Wallet、imToken、Zerion 也有不小的用户群体。总之目前的钱包软件非常的多。作为开发者我们不需要一个个去适配这些钱包。大部分钱包都实现了 WalletConnect 协议,我们只要兼容 WalletConnect 就可以变相的兼容大部分钱包。再配合 web3Modal 这个 NPM 包,简单的配置一下参数就能兼容市面上几乎所有的钱包。
接入 WalletConnect 必须要提供一个 infuraId,在 infura.io 免费创建一个项目即可拿到 infuraId,免费版本每天限制调用次数不超过10万次,超出需要付费,用户量大的话接入 WalletConnect 会多一些额外成本。如果用户选择用 MetaMask 连接就不消耗你的 infura 额度了。
import { useEffect } from 'react'
import { ethers } from 'ethers'
import Web3Modal, { CHAIN_DATA_LIST, ChainData } from 'web3modal'
import WalletConnect from '@walletconnect/web3-provider'
import CoinbaseWalletSDK from '@coinbase/wallet-sdk'
import { CoinbaseWalletSDKOptions } from '@coinbase/wallet-sdk/dist/CoinbaseWalletSDK'
// 只有 walletconnect 和 coinbase wallet 需要提供 infuraId
const infuraId = '替换为你自己的 infuraId'
const providerOptions = {
walletconnect: {
package: WalletConnect,
options: {
infuraId,
rpc: {
56: 'https://bscrpc.com', // Binance Smart Chain
97: 'https://data-seed-prebsc-2-s3.binance.org:8545', // Binance Smart Chain Testnet
137: 'https://rpc-mainnet.matic.network', // Polygon
42161: 'https://arb1.arbitrum.io/rpc', // Arbitrum One
421611: 'https://rinkeby.arbitrum.io/rpc', // Arbitrum Rinkeby
43114: 'https://api.avax.network/ext/bc/C/rpc', // Avalanche C-Chain
},
},
},
// 这个是 Coinbase Wallet
walletlink: {
package: CoinbaseWalletSDK,
options: {
appName: '你的应用名称',
appLogoUrl: '',
infuraId,
} as CoinbaseWalletSDKOptions,
},
}
function App() {
const signerRef = useRef<ethers.Signer | null>(null)
const web3ModalRef = useRef<Web3Modal | null>(null)
// 初始化 Web3Modal
useEffect(() => {
const web3Modal = new Web3Modal({
network: '', // optional
cacheProvider: true, // optional
providerOptions, // required
})
web3ModalRef.current = web3Modal
if (web3Modal.cachedProvider) {
// 如果有上次使用的钱包,自动尝试连接钱包
onConnectWallet()
} else {
// 如果没有上次使用的钱包,则让用户手动连接钱包
}
}, [])
// 连接到钱包,拿到用户钱包地址和当前选择的网络
async function connectWallet() {
const web3Modal = web3ModalRef.current!
if (!web3Modal) {
// web3Modal 不存在
return
}
const provider = await web3Modal.connect()
await provider.enable() // 调用此方法后会唤起钱包弹框询问用户是否要连接到当前站点
const web3Provider = new ethers.providers.Web3Provider(provider, 'any')
const signer = web3Provider.getSigner()
signerRef.current = signer
const [publicAddress, chainId] = await Promise.all([signer.getAddress(), signer.getChainId()])
return { publicAddress, chainId }
}
function onConnectWallet() {
return connectWallet()
.then((meta) => {
if (meta) {
console.log('拿到的钱包信息', meta)
} else {
// 没有获取到钱包信息
}
})
.catch((err) => {
localStorage.removeItem('walletconnect')
web3ModalRef.current?.clearCachedProvider()
console.error('连接钱包出错', err)
})
}
return (
<div>
<button onClick={onConnectWallet}>连接到钱包</button>
</div>
)
}
基于钱包登录的方案,用户的帐户完全由用户自己掌控,一个账户可以在多个平台使用。用户不用再担心自己的密码是否被网站泄露,帐户安全完全由自己掌控,且注册和登录过程变得异常简单。需要注意的是,自动化批量创建钱包帐户的成本非常低,必要时网站可以在登录时配合 reCAPTCHA 之类的服务过滤机器人用户,或者要求用户绑定 Gmail 之类的邮箱,以此降低垃圾账户的数量。
要获得最佳阅读体验,请访问原文 https://baiyun.me/sign-in-with-ethereum]]>
Safari 在渲染 CSS 渐变文字时如果恰好这个元素是一个 flex 容器,那么将会遇到文字变透明无法显示的 bug。绕过方法是不要在 flex 容器元素上应用渐变文字相关的属性。
经过研究,我发现这个问竟然在 6 年前就存在了。但是到了 6 年后的今天,最新的 iOS 15.4.1 和 macOS 12.3.1 版本依旧存在这个问题,Safari 依旧在浪费 Web 开发者的时间和精力去绕过这本不应该存在的问题。这让我想起了那句话:"Safari 是新时代的 IE"
我想用 CSS 实现类似下图中的文字渐变效果
代码是这样的
<h1 className="heading"><Icon /> 这是一段渐变文本</h1>
.heading {
display: flex;
align-items: center;
background-image: linear-gradient(90deg, blue, red);
-webkit-text-fill-color: transparent;
-webkit-background-clip: text;
}
以上代码实际渲染出来的文字是透明看不见的。作为开发者我们绕过这个问题的办法是不要在 flex 容器元素上应用渐变文字相关的属性。上述代码去掉 disflex: flex;
即可正常工作。
要获得最佳阅读体验,请访问原文 https://baiyun.me/2022-safari-does-not-render-css-gradient-text]]>
国内的用户获取境外手机号的方法有很多,不同的方法各有优缺点,比如 Google Voice 号码大部分情况下都是没问题的,不过由于它并不是实体卡,属于虚拟 VoIP 号码,有些场景无法使用,比如注册 Wechat(海外微信) 就不行。
最近发现澳门电信提供的大湾区预付卡相对来说是不错的选择。
大湾区预付卡有一个一卡两号的功能,开通后可以获得一个国内电信的号码,根据它的说明,如果不开通国内号码,你的开户信息是不会同步到国内,信息是不互通的。我个人是没有开通国内号码的。
卡片丢失后可以本人去澳门的线下营业厅补办,一些相对重要的服务就可以放心绑定了。并且国内去澳门的成本相对还是非常低的。
开卡后用这个澳门号码注册 TikTok 和 WeChat 试了下是完全没有问题的
要获得最佳阅读体验,请访问原文 https://baiyun.me/experiences-of-using-the-macau-telecom-prepaid-card-in-china]]>
具体方法如下,先打开 macOS 上的终端应用,执行以下命令来查看 bird 进程的 pid
ps -e | grep bird
开头的数字就是 pid,执行 kill 457
杀掉 bird 进程,文件就会立刻开始同步了。
因为 pid 每次 kill 之后会变化,有个更简单的方法是配合 pgrep 命令实现一行代码杀掉 bird 进程:
kill $(pgrep bird)
要获得最佳阅读体验,请访问原文 https://baiyun.me/fix-icloud-drive-sync-stuck]]>
操作系统默认都有上图这种系统级别的代理配置,但是像 Electron 应用,主进程的网络请求默认并不会走这个系统代理,这个默认行为很容易给用户来带不便从而惹恼用户。如果开发者要让主进程里的网络请求走系统代理,需要用一些技巧。
找到正确读取系统代理的方法并不是一帆风顺的,着实让我花了一点时间,也踩了不少可坑,最终我测试可行的方案如下,这个方案同时支持 macOS 和 windows
以下代码需要放在主线程执行
import HttpsProxyAgent from 'https-proxy-agent'
const mainWindow = new BrowserWindow({
show: false,
width: 1024,
height: 768,
})
// 读取浏览器的 session
const session = mainWindow.webContents.session
// 下面的代码会尝试解析代理配置,如果用户配置了系统代理,
// 并且代理规则没有排除 www.google.com,那我们就可以读取到代理信息
session.resolveProxy('https://www.google.com').then((proxyUrl) => {
// DIRECT 表示没有配置代理
if (proxyUrl !== 'DIRECT') {
// proxyUrl 是这种格式: 'PROXY 127.0.0.1:6152'
const hostAndPort = proxyUrl.split(' ')[1]
const [proxyHost, proxyPort] = hostAndPort.split(':')
// 到这里就拿到代理服务器的信息了,这里我用 https-proxy-agent 这个 npm 包让 Axios 的默认请求强制走系统代理
// @ts-ignore
const agent = new HttpsProxyAgent({
host: proxyHost,
port: proxyPort,
})
Axios.defaults.httpsAgent = agent
}
})
上述代码在最新的 Electron 16 版本测试通过
在 Electron 应用的主进程中,通过 Node.js 的 child_process 模块执行系统命令获取系统代理信息。不同操作系统的获取方式不同,并且随着系统升级可能出现命令不可用或者输出格式变化等不稳定情况。
在 macOS 系统上:
const { execSync } = require('child_process')
function getMacWebProxy() {
// 这种方法需要考虑到用户使用不同网卡的情况,Wi-Fi 是网络设备名称,对于使用有线网卡的用户,可能需要替换成 Ethernet 等适当的名称
const cmd = 'networksetup -getwebproxy "Wi-Fi"'
const output = execSync(cmd).toString()
const result = output.match(/(?:Enabled:\s)(\w+)\n(?:Server:\s)([^\n]+)\n(?:Port:\s)(\d+)/)
if (result) {
const [_, enabled, server, port] = result
if (enabled === 'Yes') {
return `http://${server}:${port}`
}
}
return null
}
const proxyConfig = parseProxyConfig(rawProxyInfo)
console.log(proxyConfig) // 输出代理配置字符串,如 "http://127.0.0.1:6152"
在 Windows 系统上(未验证):
const { execSync } = require('child_process')
const proxyConfig = execSync(
'reg query "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings" /v ProxyServer',
)
.toString()
.split('\r\n')
.filter((line) => line.trim())
.pop()
.split(' ')
.pop()
console.log(proxyConfig) // 输出代理配置字符串,如 "http://proxy.example.com:8080"
上面的代码只是拿到了系统代理配置,但是在用户手动改了系统代理配置的情况下,我们程序里的配置信息还是旧的,所以想要极致的用户体验,我们是需要想办法监听这个变动的。不然网络请求就会出错了。
如果你没有特殊需求,可以将网络请求放在 renderer 进程中用 xhr 或者 fetch 执行,这样网络代理会由 chromium 自动处理,开发者不需要关心如何获取和监测系统代理变化,这些复杂的部分都由 chromium 在底层帮你处理好了,就像浏览器一样。
要获得最佳阅读体验,请访问原文 https://baiyun.me/how-electron-app-get-the-system-proxy-config]]>
解决方法是,在 Safari 的实验性功能中关闭 requestIdleCallback
这个 API,然后重启 Safari 和第三方 APP,这样就可以正常用 Google 账号登录了,具体路径是:系统设置 => Safari => 高级设置(在最底部)=> 实验性功能 => requestIdleCallback (关掉开关)
参考
https://www.mobile01.com/topicdetail.php?f=627&t=6213759
要获得最佳阅读体验,请访问原文 https://baiyun.me/fix-google-account-login-stuck-in-loading-on-ios]]>
截止 2023,一年时间过去了,圈内人再次被 ChatGPT 震惊到了,与之相比,本文中的 Copilot 能力就有点小巫见大巫了。
当时准备写个清理缓存的函数,当我写完函数名的时候,copilot 自动写完了剩余部分
除了写代码,它还可以自动帮你联想注释内容,还挺准确的
要获得最佳阅读体验,请访问原文 https://baiyun.me/github-copilot-surprised-me]]>
使用本文的方法需要有一定的编程基础,且不同地区不同运营商的用户使用效果会有差距,不一定会立竿见影的效果。对于不会写代码的用户我更推荐你直接向微软客服反馈 Xbox 国行下载游戏速度非常慢,反馈的人多了微软来解决这个问题才是最好的。
要使用 xbox-speed-test,先确认以下要求是否满足:
安装 xbox-speed-test:
git clone https://github.com/isbasex/xbox-speed-test.git
cd xbox-speed-test
测试国外节点速度 (assets1.xboxlive.com
):
node index.js
测试国内节点速度 (assets1.xboxlive.cn
):
isCN=1 node index.js
测试结束后会输出各节点的速度排序表:
拿到 IP 后,在局域网内修改 DNS 记录,让 Xbox 下载强制走这个最快的 CDN 节点。
如果你用了软路由或者电脑上有 Surge、Clash、Dnsmasq 这类软件,可以很方便的修改 DNS 记录,我用 Surge 是这样配置的:
第一步:在 Surge 配置文件增加以下配置
[Host]
assets*.xboxlive.com = 23.32.241.147 // IP 指定为测试结果最快的那个
assets*.xboxlive.cn = 59.63.235.26 // 注意 .com 和 .cn 不要都指向一个 IP
dlassets.xboxlive.cn = 59.63.235.26
注意上面的配置文件我用了通配符,因为 surge 支持这种语法,对于普通 Hosts 文件这种不支持通配符语法的,你需要改成 assets1.xboxlive.com 23.32.241.147
这样的格式
第二步:在 Xbox 网络设置里选择手动,将网关地址指定为 Surge 所在电脑的 IP 地址,DNS 指定为 192.18.0.2
,网络设置好之后,将 Surge 开启增强模式就可以让 Xbox 的所有请求通过 Surge 接管。
为了确保 Xbox 下载游戏不浪费代理流量,我额外增加了如下 Surge 代理规则:
DOMAIN,assets1.xboxlive.com,DIRECT
DOMAIN,assets2.xboxlive.com,DIRECT
DOMAIN,assets3.xboxlive.com,DIRECT
详细的 Surge 使用方法可以参考:Surge for Mac之网关模式的妙用
- 根据我这段时间的体验,发现只有少部分游戏下载会走 assets*.xboxlive.cn 国内节点,而大部分游戏会走 assets*.xboxlive.com 国际节点
- 不能给 .com 域名指定 .cn 的 ip,否则会 404
- 对于使用 Windows 的用户,你可以尝试在本地修改 Hosts 文件
xbox-speed-test 这个仓库里的 cdn.list 文件内置了世界各地的 Xbox CDN 节点,这些节点随着时间的推移可能会有部分失效,你可以自行通过 dig 命令配合各国的公共 DNS 查找最新的有效 CDN 节点,也可以删除无效和特别慢的节点。
相比 PS 和 Steam,微软在 CDN 方面做的真不够好,从搜索到的信息来看好几年前就是这样。现在依旧没有改观,甚至在恶化。当两个产品整体差不多的时候。体验性细节就很重要了。
希望后续微软能尽早解决下载速度这个问题。
要获得最佳阅读体验,请访问原文 https://baiyun.me/fix-slow-xbox-download-speed]]>
对于独立网站来说有一个很大的优点就是可以被各大搜索引擎收录,进而互联网上其他用户可以通过搜索引擎发现你的内容,所以对于独立网站的站长来说你需要确保网站上新发布的内容尽可能快的被搜索引擎的爬虫找到,而不是被动的等待爬虫找到你的内容,因为对于小站点来说这个过程可能会耗费很久。
为了让搜索引擎第一时间知道你发布了新内容,大部分搜索引擎都提供了站长后台和相关 API,站长可以将网站上新产生的内容链接第一时间提交给搜索引擎。这里有一个比较麻烦的点,站长需要去调用多个搜索引擎的 API,成本就略微比较高了,所以现在微软联合 Yandex 推出了 IndexNow,它提供了一种快速的方法让网站站长可以第一时间通知所有接入 IndexNow 的搜索引擎网站的内容发生了变化。
只有网站站长才允许调用 IndexNow API,所以当你调用 IndexNow API 的时候,它会先验证你是站长才会处理你的抓取请求。要通过验证,你需要按照如下步骤操作:
qaxfMzelYq5xKx4xhEDb5ZahikSevh
qaxfMzelYq5xKx4xhEDb5ZahikSevh.txt
的文件,文件内容填写 qaxfMzelYq5xKx4xhEDb5ZahikSevh
即可https://<your_domain>/qaxfMzelYq5xKx4xhEDb5ZahikSevh.txt
返回的是 qaxfMzelYq5xKx4xhEDb5ZahikSevh
即表示设置正确除了在网站根目录放置 key 文件,也可以选择直接用 Nginx 来实现,在 Nginx 的 server 配置里增加下面的代码即可:
location = /qaxfMzelYq5xKx4xhEDb5ZahikSevh.txt {
return 200 'qaxfMzelYq5xKx4xhEDb5ZahikSevh';
}
所有权验证准备好之后,就可以调用 IndexNow API 通知搜索引擎抓取你的内容了,请求示例如下:
POST /IndexNow HTTP/1.1
Host: www.bing.com
Content-Type: application/json
{
"host": "https://baiyun.me",
"key": "qaxfMzelYq5xKx4xhEDb5ZahikSevh", // 这里的 key 需要和上面的保持一致
"urlList": [
"https://baiyun.me/using-indexnow",
"https://baiyun.me/about"
]
}
API 调用成功会返回 200 响应码,响应主体为空:
调用成功后,你的抓取请求就通知到所有接入 IndexNow 的搜索引擎了,它们会尽快来抓取页面,需要注意的是,抓取页面并不代表它们会收录你的页面,只有收录成功之后你的内容才会出现在搜索结果中。具体要不要收录,各大搜索引擎都有自己的算法,网站站长能做的就是尽量让网站上的内容是『有价值』的。
Cloudflare 有一个 Crawler Hints 功能现在已经支持 IndexNow,如果你的网站使用了 Cloudflare,可以尝试开启 Crawler hints 开关,具体路径如下:登录并选择你的站点 -> 点击导航栏的"缓存" -> 点击配置 -> 下拉找到 Crawler Hints -> 打开开关即可
Crawler Hints 这个功能目前还是测试阶段,具体效果如何还有待商榷。
IndexNow 目前只支持 Microsoft Bing 和 Yandex,目前官方说法是老大哥 Google 并没有计划支持 IndexNow,所以如果你想兼顾其他搜索引擎,可以考虑调用这些搜索引擎各自提供的 API 来实现,已经有人将这些 API 封装成公共模块,比如 submit-url 就支持百度、谷歌、必应。
我观察了几天本站内容在各大搜索引擎的收录速度,一个新链接,从提交给搜索引擎,到最终在搜索结果中显示出来,Google 是最快的,通常只需要不到 1 天的时间,其次是 Yandex,需要 1-2 天左右,再其次是 Bing,时间不确定,至于百度,嗯。不说了。
经过长时间的观察,我发现 Google 的索引速度和数量都领先其他搜索引擎,对用户来说同样的关键字在 Google 可以更及时的搜索到更多的内容。
要获得最佳阅读体验,请访问原文 https://baiyun.me/using-indexnow]]>
要解决这个问题,我尝试调整了 Surge 的很多参数,甚至和 AmpliFi 的 24 小时客服在线交流许久,也给 Surge 开发者发了邮件反馈,都没有直接解决我的问题,Surge 开发者告知是 AmpliFi 的问题,双方都在踢皮球。
最终在研究了小火箭🚀的默认配置后,找到了可行的解决方法。
在 Surge 配置文件中加入下面的内容,让保留的 IP 段不走 Surge 的虚拟网卡(Virtual Network Interface,简写为VIF) 即可。
[General]
tun-excluded-routes = 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.0.0.0/24, 192.0.2.0/24, 192.88.99.0/24, 192.168.0.0/16, 198.18.0.0/15, 198.51.100.0/24, 203.0.113.0/24, 224.0.0.0/4, 255.255.255.255/32
现在开启 Surge 可以正常使用 AmpliFi 了。
2021-11-26 更新:上述 Surge 配置可以让 AmpliFi 正常工作,但是会导致国内某些应用无法正常工作,比如叮咚买菜和微信授权跳转就有问题,经过测试,发现只用
tun-excluded-routes = 255.255.255.255/32
就可以解决上述问题。
像 Surge 这类配置项非常多的软件,开发者还是应该默认提供一份对大多数用户友好的默认配置或者配置建议,而不是完全让用户自己摸索。
要获得最佳阅读体验,请访问原文 https://baiyun.me/ios-surge-and-amplifi]]>
图片托管是一个很常见的需求,经常能看到不少人在想方设法使用类似微博图床、GitHub issues 等方案变相的当作免费图床,实际上这些方法很容易违反对方的服务条款,说不准哪天就把你的图片删了或者限制外链到其他网站访问,这种是非常不推荐的。
本文用 Amazon S3 和 Cloudflare 举例如何基于对象存储服务和 CDN 自建一个可靠的低成本静态文件托管服务。
这套方案的核心是利用 S3 这类对象存储服务解决文件存储的可靠性和安全性,再利用 Nginx 反向代理和缓存,配合 CDN 极大降低流量成本。
本文不是面向完全新手的,需要你有一定的相关经验
在前置条件全部准备好之后,参考如下 Nginx 配置,配置好反向代理和缓存。
# 先创建缓存目录,并设置正确权限,确保 nginx 有权限读写
mkdir -m 777 /var/cache/nginx
http {
# 最多缓存 1000M, 缓存时间 30天
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=s3_cache:100m max_size=1000m inactive=30d;
server {
listen 443;
server_name your_domain; # 替换成你自己的域名
# s3 资源反向代理
location ~ ^/resource/(.+)$ {
proxy_hide_header x-amz-id-2;
proxy_hide_header x-amz-request-id;
proxy_hide_header x-amz-meta-server-side-encryption;
proxy_hide_header x-amz-server-side-encryption;
proxy_hide_header Set-Cookie;
proxy_ignore_headers Set-Cookie;
proxy_set_header Connection "";
proxy_set_header Authorization "";
proxy_set_header Host your_bucket_name.s3.eu-west-2.amazonaws.com;
proxy_cache s3_cache;
# 如果源站响应状态码大于 300, 则返回 nginx 自己的错误页面,避免泄露源站信息
proxy_intercept_errors on;
proxy_cache_revalidate on;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_lock on;
# 状态码为 200 则缓存 30 天
proxy_cache_valid 200 30d;
# 用户浏览器和CDN都缓存 365 天
add_header Cache-Control "max-age=31536000";
add_header X-Cache-Status $upstream_cache_status;
proxy_pass https://your_bucket.s3.eu-west-2.amazonaws.com/$1;
}
}
}
上面配置文件中的 your_bucket.s3.eu-west-2.amazonaws.com
需要替换成你自己的 S3 Endpoint
配置文件保存后执行 nginx -t
可以检查语法是否正确,如果没问题,执行 service nginx reload
让配置生效。
Nginx 和 Cloudflare 都配置完成之后,通过你的域名访问 S3 上的资源会有两层缓存,大部分请求会被 CDN 和 Nginx 缓存拦截下来,不会回源到 S3 源站,所以你只需要支付 S3 的存储费用和少量流量费用,只要你的流量不是太夸张,Cloudflare 默认不会收取你的流量费用。
注意,本文这里设置缓存日期是 30 天,对于大部分情况下是没问题的,如果你希望 S3 的文件删除后,后续请求不返回缓存的文件,那么就要自己再做一些定制了。
使用 S3 通常都是按实际使用流量付费的,如果你不想一觉醒来收到天价账单,就需要做一些防止刷流量的措施,防范于未然,常用的措施有以下几种:
默认情况下如果别人知道了你的 Bucket 名称和 Endpoint 地址是可以直接绕过 CDN 直接访问 S3 源站的,这样就有被刷流量的风险,我们可以使用 S3 的存储桶策略来增强安全性。
具体配置路径是:进入 S3 控制台 -> 选择 Bucket -> 点击 权限 Tab -> 下拉找到 存储桶策略
点击编辑按钮,参考下面的配置,可以将 aws:Referer 设置成一个随机生成的字符串,这样后续访问 S3 如果没有携带这个 Referer 值就会被 S3 拦截
{
"Version": "2012-10-17",
"Id": "Policy1633794210209",
"Statement": [
{
"Sid": "只允许指定的 Referer 访问公共资源",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": ["arn:aws:s3:::your_bucket/dir1/*", "arn:aws:s3:::your_bucket/dir2/*"],
"Condition": {
"StringNotEquals": {
"aws:Referer": "替换成任意字符串,你可以随机生成一个ID放在这里"
}
}
}
]
}
上面的 Resource 请替换成你自己的 Bucket 名称和目录
S3 安全策略配置完成后,需要在 Nginx 反向代理配置中用 proxy_set_header 指令加上 Referer 请求头,这样只有通过你的 Nginx 服务器才能访问 S3 的公共资源
location ~ ^/resource/(.+)$ {
...
proxy_set_header Referer "替换成S3存储桶策略里填写的 Referer 值";
...
proxy_pass https://your_bucket_name.s3.eu-west-2.amazonaws.com/$1;
}
相关链接
要将文件上传到 S3,通常有以下几种方法:
如果你想实现自己的上传逻辑,可以参考:
Q: 上传到 S3 的文件无法访问提示没权限
A: 请检查对应文件的访问权限是否设置正确
最近(2021-10)Cloudflare 接连推出 Cloudflare Images 和 Cloudflare R2 Storage这两个服务,如果你是 Cloudflare 重度用户,可以研究下这两个服务,官方说法是相比传统方案它可以消除数据在不同服务商之间流转造成的大量流量出口费用。
要获得最佳阅读体验,请访问原文 https://baiyun.me/setup-image-hosting-s3-nginx]]>
最近发现不少新手或有一定 TS 使用经验的的开发同学并没有充分发挥 TS 枚举值的能力,代码中很容易见到 if(status === 1){} else if (status === 2){}
这种魔法数字,项目只要稍微复杂一些,这种魔法数字维护起来就是一场灾难。
类似上图中的枚举状态在开发中很常见,我们可以使用 TS 的枚举类型来提高维护性,减少 bug
bad:
if (status === 1) {
//
} else if (status === 2) {
//
}
good:
export enum TaskStatus {
// 处理中
processing = 0,
// 成功
success = 1,
// 异常
error = 2,
}
if (status === TaskStatus.processing) {
// 处理中
} else if (status === TaskStatus.success) {
// 成功
} else if (status === TaskStatus.error) {
// 异常
} else {
// 无效的枚举值
}
当然你也可以使用 switch 语句代替上面的写法。
有时候你可能需要在一些全局的 .d.ts
类型声明文件中引用其他模块中定义的枚举值,这种情况可以通过 import() 语句来引入其他枚举值而不破坏 .d.ts
文件的作用范围,例如
Bad:
// xxx.d.ts
import { AppType } from '@/componets/AppCard'
declare interface AppState {
name: string
type: AppType
}
Good:
// xxx.d.ts
declare interface AppState {
name: string
type: import('@/componets/AppCard').AppType
}
要获得最佳阅读体验,请访问原文 https://baiyun.me/improve-maintainability-with-typescript]]>
有几个点需要科普下:
Asia/Shanghai
、America/New_York
这种格式+8
、-5
这种格式是当前时间相对于 UTC 时间的偏移量(UTC offset),不要把它和时区认为是同一个概念,为什么呢,因为某些地区的时间是有夏令时和冬令时的,也就是说同一个地区,它的偏移量在冬天和夏天是不一样的如果你对时区这个话题非常感兴趣,可以继续阅读以下文章:
要获得最佳阅读体验,请访问原文 https://baiyun.me/javascript-timezone]]>
不过进入 Web 2.0 时代就不一样了,因为这个时候 Web 开发者可以利用 Ajax 来实现页面的局部刷新,但是网站的用户通常是分布在不同的地理位置并且他们的网络环境差异也是非常大的,这就导致了同样的操作,有些用户可能马上可以看到结果反馈,有些用户就要等比较久的时间才能看到操作结果。
如果用户在页面上点击了一个按钮,但是等了很久页面都没有看到任何反馈,这个时候给用户的感觉就是页面卡死了,或者停止响应了,这种结果对用户的心理体验是非常不利的,所以这个时候就要加入一个 Loading 动画来即时的对用户操作进行响应,例如这种效果:
通过增加 Loading 动画,可以避免让用户产生页面卡死或者没有响应的心理反应,不过 Loading 动画在某些场景下也会带来新的问题。
如前面所说,用户会分布在不同的地理位置,并且拥有不同的网络环境(比如 光纤、4G、3G),这就导致他们的网络延迟是截然不同的,如果一个网站部署在美国西海岸,在不考虑服务器响应速度的情况下,美国西海岸的用户从发起请求到收到响应大概需要 100ms 左右, 而中国用户访问这个网站,同样的请求因为网络延迟的关系就要 2s 左右,这个时间在恶劣的网络环境下会更久。
想象一个场景,现在网站上有一个文章标题列表,点击其中一篇文章的标题后会跳转到文章页面,按照上面所说,美国西海岸的用户点击之后只需 100ms 就可以看到新的内容,中国用户点击后至少需要 2s 以后才能看到新内容,如果在用户点击按钮之后立即显示 Loading 动画,对于那些网络延迟足够低的用户来说就会出现类似下面这样的场景:
可以看到,Loading 动画一闪而过,给人的一个心理体验是非常不好的。
2021 更新,感受一下某国内热门 app 的 web 版
从实际的用户心理角度讲,打开一个页面如果三秒后还是 Loading 状态,这个时候用户就会产生不耐烦的感受,甚至一些用户在这种场景下可能会直接关闭页面而不会继续等待。
根据前面的情况,我们设定一个理想的 Loading 动画展示条件:
根据上面的条件,用 React 进行实现:
function Loading(props) {
const { isLoading, children, delay, minDuration } = props
const [visible, setVisible] = useState(false)
const startTime = useRef(0)
useEffect(() => {
const remaining = minDuration - (Date.now() - startTime.current)
const timeout = isLoading ? delay : remaining >= 0 ? remaining : 0
const timer = setTimeout(() => {
setVisible(isLoading)
if (isLoading) {
startTime.current = Date.now()
} else {
startTime.current = 0
}
}, timeout)
return () => {
clearTimeout(timer)
}
}, [isLoading])
if (visible) {
return <SkeletonScreen />
}
return children === undefined ? null : children
}
Loading.defaultProps = {
// 用户触发异步操作后需要等 400ms 才会显示 Loading 动画
delay: 400,
// 如果展示了 Loading 动画,至少要展示 700ms
minDuration: 700,
// 异步操作是否正在进行中
isLoading: false,
}
改进后的 Loading 效果:
可以看到改进之后,网络延迟足够低的情况下用户不会看到 Loading 动画,当网络延迟高的时候才会看到。
Loading 动画是把双刃剑,如果你的应用响应速度足够快,完全可以在大部分情况下不需要显示 Loading 动画。
本文从以下两个角度来说明如何提高 React 应用的响应速度:
1. 尽可能早的开始加载数据和代码
在 React 应用里常见的请求数据方式是这样实现的:
// pageA.jsx (这个文件会被异步加载)
function PageA(props) {
const [data, setData] = useState(null)
useEffect(() => {
requestArticleData(props.id).then((res) => setData(res.data))
}, [props.id])
return <div>{data ? <Article {...data} /> : null}</div>
}
这种方式的问题在于,它需要等到 PageA.jsx
这个文件加载完成,并执行完上面的代码,等 React 组件挂载之后才会开始请求这个页面需要的数据,它是这样一个串行的过程:
有一种更好一点的方法是这样的:
要实现提前加载页面需要的数据,可以把加载数据的逻辑提取出来放在一个静态方法中,这样在用户请求切换到 PageA 的时候就可以通过这个静态方法来提前加载这个页面需要的数据,这种方法在 Next.js 这类框架中被广泛使用。
function PageA(props) {
return <div>{props.data ? <Article {...props.data} /> : null}</div>
}
PageA.getInitialProps = async ({ query }) => {
const data = await requestArticleData(query.id).then((res) => res.data)
return {
data,
}
}
2. 合理使用缓存策略
在一些实际场景中,数据变化的频率是非常低的,这种情况在第一次通过网络请求到数据后可以缓存起来,后续再次访问这个资源的时候可以直接从缓存读取。
但是一旦使用了缓存,就要考虑很多额外的问题,比如缓存的 key、时效性,好在现在很多工具库(react-query、urql、react-apollo)都内置了一些开箱即用的缓存策略:
具体使用什么策略需要根据具体的场景进行取舍,除了上面的这些工具库外,还可以看情况使用 Service Wroker,它也有类似上面这些缓存策略。
React 一直在致力于打造更好的用户体验,在开启 React 并发模式之后可以通过 Suspense 和 useTransition 更简单直观的来处理异步状态,不过截止目前相关 API 还处于实验性阶段,如果你感兴趣可以查看 React 官方提供的文档了解更多信息 。
相关资源:
要获得最佳阅读体验,请访问原文 https://baiyun.me/improve-the-user-experience-for-react-apps]]>
默认情况 v8 限制堆内存最大可分配空间为 1.4GB,对于特别耗内存的应用,可以按照下面的示例增大可用空间,避免内存耗尽导致的程序自动退出。
// old_space 调整为 4GB
NODE_OPTIONS="--max_old_space_size=4096" node index.js"
https://nodejs.org/api/cli.html#cli_node_options_options
要获得最佳阅读体验,请访问原文 https://baiyun.me/nodejs-tips]]>
2020-10-27 更新:Sass 官方已在 2020-10-26 正式宣布弃用 LibSass,并推荐使用 Dart Sass https://sass-lang.com/blog/libsass-is-deprecated
对于使用 Scss 的前端项目,经常会在 yarn install 过程中出现 node-sass 安装失败的情况,又或者切换了 Node.js 版本发现 node-sass 需要编译才能用,如果使用 alpine 版本的 docker 镜像安装 node-sass 还会遇到由于缺少 Python 和各种依赖导致 node-sass 编译失败的情况,又或者在国内由于网络原因导致 node-sass 需要的二进制文件下载不下来而 build 失败。
那么为什么 node-sass 会有这么多问题呢,这就要涉及到另一个项目: LibSass,在很长一段时间里,用 C/C++ 写的 LibSass 一直是 Sass 语言的一个主流实现,其他语言如果要使用 LibSass,需要有建立一个 wrapper 才可以,在 Node.js 环境里,这个 wrapper 就是广为人知的 node-sass,它的作用就是在 Node.js 环境里用 JavaScript 去调用 LibSass,问题的本质还是在 Node + LibSass 造成的复杂性。
上面的这些问题一直广受 JS 社区广大用户的诟病,Sass 官方也意识到了这个问题,所以现在 Sass 官方宣布使用 Dart Sass 作为 Sass 的主要实现:
Dart Sass is the primary implementation of Sass, which means it gets new features before any other implementation. It's fast, easy to install, and it compiles to pure JavaScript which makes it easy to integrate into modern web development workflows.
和 LibSass 不同,Dart Sass 最终是被编译成纯粹的 JS 代码来执行的,所以如果使用 Dart Sass,上面那些使用 node-sass 出现的问题就不会再遇到了,另外 Dart Sass 和 node-sass 在暴露给用户的 API 方面是保持一致的,这表示几乎可以无痛迁移。
sass-loader 从 9.0.0
版本开始已经默认使用 Dart Sass 了,用户不需要做任何操作,如果你因为一些原因暂时无法升级 sass-loader 到 v9
版本,可以参考下面的做法来手动启用 Dart Sass。
这里用 webpack 举例:
# 记得删掉 node-sass
yarn remove node-sass
# 安装 dart sass (不要问我为什么包名是 sass)
yarn add sass -D
# 如果你的 sass-loader 版本小于 7.2,需要手动更新到 >= 7.2 的版本
yarn add sass-loader@7
修改 webpack 配置,在 sass-loader 的 options 里加一行 implementation: require('sass')
{
loader: 'sass-loader',
options: {
implementation: require('sass'), sourceMap: true
}
}
这样就配置好了,使用上感受不到差异,不过再也不会遇到 node-sass 安装的各种问题了,性能方面,我在自己的项目里也进行了多次测试对比,从我的测试结果和 Dart Sass 给出的 perf 来看,总体性能和 libsass 相比几乎一致,完全不用担心。
要获得最佳阅读体验,请访问原文 https://baiyun.me/replace-node-sass-with-dart-sass]]>
创建 /etc/wsl.conf
[automount]
options = "metadata,umask=22,fmask=11"
在 .zshrdc 中添加
umask 0022
解决方法:管理员权限启动 cmd,执行:
netsh winsock reset
https://adamtheautomator.com/windows-subsystem-for-linux/amp/#
要获得最佳阅读体验,请访问原文 https://baiyun.me/wsl-problem-and-solutions]]>
在本地调试 next.js 过程中你的项目和 next.js 要使用相同的react 和 react-dom 实例
操作方法:
也可以反过来将 next.js 里的 react 和 react-dom link 到自己的项目里
npm ls react
要获得最佳阅读体验,请访问原文 https://baiyun.me/npm-and-yarn-tips]]>
git revert --strategy resolve <commit>
# 删除远程分支 serverfix
git push origin :serverfix
# 记住用户名密码(明文保存)
git config --global credential.helper store
git shortlog -sn
要获得最佳阅读体验,请访问原文 https://baiyun.me/git-tips]]>
在 linux 上可以用 netstat
这个命令很方便的查看端口占用和开放情况。在 macOS 上不支持这个命令,要查看端口就比较蹩脚了。
下面的命令可以找出某个端口被哪个进程占用
sudo lsof -Pn -i4 | grep LISTEN
从 macOS Monterey 12 开始,系统自带了一个 networkQuality
命令用于测试网络带宽,我试了下挂代理的情况下可以跑满我家 500M 带宽,国内直连的话速度就非常慢了,结论是除非苹果增加国内节点否则这个命令只适合测试梯子的速度。国内宽带测速还是得用 speedtest
networkQuality
要获得最佳阅读体验,请访问原文 https://baiyun.me/macos-cli-tips]]>
https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity
script 标签加上 integrity 和 onerror 可以来确认是否被劫持
将 .js 后缀去掉可以防止通过检测 .js 后缀来劫持
https://www.zhihu.com/question/35720092/answer/523563873
要获得最佳阅读体验,请访问原文 https://baiyun.me/javascript-prevent-hijacking]]>
要获得最佳阅读体验,请访问原文 https://baiyun.me/about-dota2]]>
要获得最佳阅读体验,请访问原文 https://baiyun.me/macos-system-maintenance-apps]]>
apt install gconf-service libasound2 libatk1.0-0 libatk-bridge2.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget
这些发行版的 server 版本通常是没有预装中文字体的,网页里的中文部分会变成方块,所以还需要先安装中文字体
# 这里的 fonts 目录需要根据你的实际情况自行调整,你也可以直接将字体放到 /usr/share/fonts/truetype/ 目录下
cp -R fonts/* /usr/share/fonts/truetype/
fc-cache -f -v
然后就可以创建项目目录和安装 puppeteer 了:
mkdir puppeteer-demo
cd puppeteer-demo
yarn add puppeteer
touch index.js
编辑 index.js 为如下内容:
async function printPDF() {
const browser = await puppeteer.launch({ args: ['--no-sandbox'] }) const page = await browser.newPage()
await page.setViewport({ width: 1920, height: 1200 })
await page.goto('http://www.qq.com/')
await page.pdf({
path: 'page.pdf',
format: 'Tabloid',
printBackground: true
})
await browser.close()
}
printPDF()
如果是以 root 用户启动 puppeteer,需要增加 --no-sandbox
启动参数
const browser = await puppeteer.launch({ args: ['--no-sandbox'] })
最后执行 node index.js
完毕就可以看到当前目录下的 page.pdf 文件了
相关资料
要获得最佳阅读体验,请访问原文 https://baiyun.me/use-puppeteer-to-print-pdf-on-linux]]>
黄金圆环法则主要是三个由外向内的问题:
很多技术产品的文档,上来就开始讲 how,而完全没有解释对新用户解释 what 和 why。这是一种很糟糕的体验。好的技术文档应该遵循黄金圆环法则。
我们需要经常检视自己,是否在工作和生活中很少思考 why。如果是的话,就要格外当心了。
要获得最佳阅读体验,请访问原文 https://baiyun.me/the-golden-circle]]>
就是一个命名法则,通过前缀来隔离作用域。
举个例子,.button-text-active
这个 class 用于描述一个按钮中的文本处于 active 状态下。
一把利器,通过在构建时自动对 class 名称进行 hash,让开发者不再担心命名冲突问题。
在 JS 里面写 CSS 很符合 React 社区 All in JS 的风潮。
原子化 CSS,变着花样的玩,核心优势是写样式非常快,且生成的 css 体积非常小几乎没有重复,对优化首屏渲染非常有效。
以上各种方案都用过,在大部分业务中场景我还是喜欢 CSS Modules 的简单靠谱,当然其他方案也有各自适合的用武之地,比如写组件库可以考虑用 BEM 或者 CSS in JS,写活动页面就可以用 Tailwind CSS 快速出活。
要获得最佳阅读体验,请访问原文 https://baiyun.me/css-evolution]]>
结论是能在 macOS 上开发就用 macOS,Linux 也可以。Windows 对于习惯了 Unix/Linux 的 web 开发者来说太蛋疼了。
要获得最佳阅读体验,请访问原文 https://baiyun.me/windows10-dev-environment-setup]]>
如果你想在用户触发特定交互后触发 debugger 立即暂停,可以在控制台执行下面的代码,指定时间后页面就会触发 debugger,页面会完全暂停。
setTimeout(() => {
debugger
}, 1000)
要获得最佳阅读体验,请访问原文 https://baiyun.me/chrome-devtools-tips]]>
比如,在不使用任何设计模式的情况下,要实现一个类似这样的 Tabs 组件
最初的代码可能是这样的
const Tab = ({ value, active, onChange, children }) => (
<div className={active && 'active'} onClick={evt => onChange(value, evt)}>
{children}
</div>
)
用起来是这样的:
class Index extends React.Component {
state = {
value: 'a'
}
handleChange = value => {
this.setState({ value })
}
render() {
return (
<div>
<Tab value="a" active={this.state.value === 'a'} onChange={this.handleChange}>
Tab A
</Tab>
<Tab value="b" active={this.state.value === 'b'} onChange={this.handleChange}>
Tab B
</Tab>
<Tab value="c" active={this.state.value === 'c'} onChange={this.handleChange}>
Tab B
</Tab>
</div>
)
}
}
从上面的代码可以看出,Tab 组件的 Props 中只有 value
和 children
是由使用者决定的,active
和 onChange
是有固定模式且重复的,我们可以想一些办法将这些有清晰模式且重复的东西自动传给 Tab 组件,避免我们每次都手动维护
这种模式利用 React.Children 这个 API, 给每个 Tab 组件自动传入 value
和 active
props,这样我们使用 Tab 组件时只需要提供 value
和 children
就可以了
const Tabs = ({ value, onChange, onClick, children }) =>
React.Children.map(children, el => {
const { value: childValue } = el.props
return React.cloneElement(el, {
onChange,
onClick: onChange,
active: value === childValue,
value: childValue
})
})
用起来是这样的:
class Index extends React.Component {
state = {
value: 'Tab A'
}
handleChange = value => {
this.setState({ value })
}
render() {
return (
<Tabs value={this.state.value} onChange={this.handleChange}>
{['Tab A', 'Tab B'].map(tab => (
<Tab value={tab} key={tab}>
{tab}
</Tab>
))}
</Tabs>
)
}
}
这种模式一般都会限制 DOM 结构,比如:
针对上面的问题,我们可以用 React 16
新增的 context API 来重写上面的例子
const initialValue = {
value: '',
onChange() {}
}
const TabsContext = React.createContext(initialValue)
const Tab = ({ value, children }) => (
<TabsContext.Consumer>
{ctx => (
<div className={ctx.value === value && 'active'} onClick={evt => ctx.onChange(value, evt)}>
{children}
</div>
)}
</TabsContext.Consumer>
)
const Tabs = ({ value, onChange, children }) => (
<TabsContext.Provider value={{ value, onChange }}>{children}</TabsContext.Provider>
)
现在可以这样使用:
class Index extends React.Component {
state = {
value: 'Tab A'
}
handleChange = value => {
this.setState({ value })
}
render() {
return (
<Tabs value={this.state.value} onChange={this.handleChange}>
{['Tab A', 'Tab B'].map(tab => (
<span key={tab}>
<Tab value={tab}>{tab}</Tab>
</span>
))}
</Tabs>
)
}
}
上面的代码里 Tabs 组件内可以随意嵌套组合其他组件,并不会影响 Tab 组件正常工作
这种模式主要的优点在于可以 props 透传,可以让 DOM 结构更灵活。
Render Props 模式本质上相当于控制反转。在前面的 Tab 组件里可以看到它返回了一个 div
元素,但在有些情况下用户会想要用其他组件来代替这个 div
元素,这种情况可以使用一个 callback
作为 prop,将 Tab 组件的核心状态作为参数传给它,用户拿到这些核心状态后到底是渲染 div
还是渲染其他组件就完全交给用户来决定了
将开头的 Tab 组件修改为如下:
const Tab = ({ value, active, onChange, children, render }) => {
if (typeof render === 'function') {
return render({ value, active, onChange })
}
return (
<div className={active && 'active'} onClick={evt => onChange(value, evt)}>
{children}
</div>
)
}
现在可以自定义 render 内容了:
class Index extends React.Component {
state = {
value: 'Tab A'
}
handleChange = value => {
this.setState({ value })
}
render() {
return (
<Tabs value={this.state.value} onChange={this.handleChange}>
{['Tab A', 'Tab B'].map(tab => (
<Tab
value={tab}
render={({ value, active, onChange }) => ( <li className={active && 'active'} onClick={evt => onChange(value, evt)}> <span>{tab}</span> </li> )} key={tab}
/>
))}
</Tabs>
)
}
}
这种模式可以很好的提取纯粹的逻辑组件,最大化的复用业务逻辑
所谓高阶组件就是一个函数,它可以接收一个组件并返回一个组件,它可以用来装饰组件,给组件注入 props
例如,我们整个应用中有几个不同地方都散布着几个类似下面这样的的组件:
const NavBar = ({ user }) => (
<nav>
User: <span>{user ? user.name : 'fetching...'}</span>
</nav>
)
这些组件都有一个相同的 user prop,这个 user 是需要从其他地方动态获取的,为了避免获取 user 的操作重复,我们可以把它提取为一个 HOC:
const withUser = WrappedComponent =>
class HocWithUser extends React.Component {
state = {
user: null
}
componentDidMount() {
fetchUser().then(user => {
this.setState({ user })
})
}
render() {
return <WrappedComponent {...this.porps} user={this.state.user} />
}
}
然后可以这样使用:
const NavBar = ({ user }) => (
<nav>
User: <span>{user ? user.name : 'fetching...'}</span>
</nav>
)
const NavBarWithUser = withUser(NavBar)
class Index extends React.Component {
render() {
return <NavBarWithUser />
}
}
通过这个 HOC 我们可以将 user 注入给指定的组件,而不必重复 user 获取逻辑,但是目前还有两个问题:
withUser
返回的组件在 ReactDevTools
里显示的名称都是相同的 HocWithUser
,这会影响我们的 Debug 效率withUser
返回的组件是无法访问到 WrappedComponent
的静态属性,这显然是不行的针对第一个问题,通常做法是在 HocWithUser
增加一个静态属性 displayName
,如下:
const withUser = WrappedComponent =>
class HocWithUser extends React.Component {
static displayName = `withUser(${WrappedComponent.displayNanme || WrappedComponent.name || 'Component'})`
state = {
user: null
}
componentDidMount() {
fetchUser().then(user => {
this.setState({ user })
})
}
render() {
return <WrappedComponent {...this.porps} user={this.state.user} />
}
}
针对第二个问题,我们可以遍历 WrappedComponent
,将它的静态属性添加到 HocWithUser
,但是要排除 React 相关的属性,这个操作可以用 hoist-non-react-statics 或者 recompose/hoistStatics 代劳
这里使用后者,将前面的 withUser
作为 hoistStatics
的第一个参数即可:
import hoistStatics from 'recompose/hoistStatics'
const withUser = hoistStatics(
WrappedComponent =>
class HocWithUser extends React.Component {
static displayName = `withUser(${WrappedComponent.displayNanme || WrappedComponent.name || 'Component'})`
state = {
user: null
}
componentDidMount() {
fetchUser().then(user => {
this.setState({ user })
})
}
render() {
return <WrappedComponent {...this.porps} user={this.state.user} />
}
}
)
这里的 user 在实际应用中通常是整个应用共享的,这种情况实际上使用前面提到的
context
模式比较合适,当然context
模式也可以和HOC
模式很好的配合
在 React 应用里,受控组件也叫木偶组件,就是这个组件的 value 都是父组件控制的,父组件传给它什么内容,它就展示什么,当内容发生变化的时候,通过调用父组件传下来的 onChange 回调函数通知父组件内容发生了变化。
这种模式的好处是可以将展示逻辑和控制逻辑分离,并且可以方便组件之间进行组合复用。
一般情况下受控组件的 props 看起来是这样的
type Props = {
value: boolean
onChange: () => void
}
使用的时候通常需要这样:
class Container extends React.Component {
state = {
value: false
}
handleChange = () => {
this.setState(prevState => ({ value: !prevState.value }))
}
render() {
return <ControlledComponent value={this.state.value} onChange={this.handleChange} />
}
}
受控组件必须配合容器组件或者 hooks 来使用,有时候难免会让人觉得啰嗦,因为不是任何场景都需要受控组件,所以我们的目标是让组件又可以受控,也可以不受控,这样使用起来就既灵活也方便。
在上面的例子中可以利用 value
这个 prop
来判断是否应该是一个受控组件,如果 value
存在那就使用这个值,需要改变值得时候就调用 onChange
让父级组件自己决定怎么改变 value 的值。
value 存在的时候这个组件是受控组件,不存在的时候就是非受控组件。
class MyComponent extends React.Component {
state = {
value: false
}
get value() {
return 'value' in this.props ? this.props.value : this.state.value
}
handleClick = () => {
const { onChange } = this.props
if (onChange) {
onChange()
} else {
this.setState(prevState => ({ value: !prevState }))
}
}
render() {
return <div onClick={this.handleClick}>state: {this.value}</div>
}
}
上面的模式可以在这里查看 在线 Demo
从前面的例子可以看出,在 React Hooks 出现之前,逻辑复用主要依靠 HOC 和 Render Props,但是这两种模式本质上都是都过包一层组件来实现的,有一个很致命的问题是嵌套,如果你有10个 HOC 或者 Render Props 实现的逻辑组件,那么当你需要同时使用这 10个逻辑组件的时候,就会出现嵌套地狱的情况。
<CurrentUserQuery>
{(currentUserQuery) => (
<SettingsQuery user={currentUserQuery.data}>
{(settingsQuery) => (
<ThemeQuery user={currentUserQuery.data}>
{(themeQuery) => (
<ProjectsQuery user={currentUserQuery.data}>
{(projectsQuery) => (
projectsQuery.data &&
projectsQuery.data.map(p => (
<Project {...p} settings={settingsQuery.data} theme={themeQuery.data} />
)
)}
</ProjectsQuery>
)}
</UserThemeQuery>
)}
</UserSettingsQuery>
)}
</CurrentUserQuery>
除此之外,在 React Hooks 之前,函数式组件并没有生命周期的概念,所以副作用和生命周期都得放到 class 组件中完成,但是 class 组件的生命周期在使用上也是有很多痛点和心智负担的,比如在 didMount 里面监听了一个事件,那么你要记得在 unmount 生命周期里清除这个事件监听,否则就容易造成内存泄露等问题。还有在 class 组件里面监听一个值的变化也非常麻烦不直观,你需要在 didUpdate 里面手段做很多判断逻辑。
因为以上多种问题,最终 React 团队在 16.8 这个版本正式推出了革命性的 React Hooks,它不仅可以让传统的函数式组件具备 class 组件的能力,同时更有意义的是,它让 React 更加函数式,在一定程度上改变了开发者的思维习惯,让组件的状态和副作用之间的同步关系更加清晰明确。当然,Hooks 也不是银弹,它也带来了额外的一些问题。
hooks 参考资料:
要获得最佳阅读体验,请访问原文 https://baiyun.me/react-design-patterns]]>
本文大部分命令在 macOS 上同样适用,对于系统没有内置的命令可能需要你先用 brew 安装
nmap -p 80 qq.com
file.sh >> file.log 2>&1
# 将当前目录下所有 .js 文件重命名为 .ts ( 包括子孙目录 )
find . -depth -name "*.js" -exec sh -c 'mv "$1" "${1%.js}.ts"' _ {} \;
Use find:
find . -name "*.bak" -type f -delete
But use it with precaution. Run first:
find . -name "*.bak" -type f
to see exactly which files you will remove.
Also, make sure that -delete is the last argument in your command. If you put it before the -name *.bak argument, it will delete everything.
# 删除 dist 目录下包含的所有 __tests__ 目录
find dist -name "__tests__" -type d -exec rm -rf {} +
# 显示用户自己的进程树
ps -auf
# 显示所有进程
ps -ef
在脚本开头设置 PATH
变量可以决定后面的命令和子 shell 中的命令去哪里查找可执行程序
使用 export
命令导出环境变量可以确保后面执行的程序和子 shell 中可以读取到这个环境变量,在 shell 脚本中使用 export
命令导出的环境变量仅在这个脚本的执行过程中有效,并不会影响到外部环境
最好在脚本开头加上一行 set -eux
,可以打印出执行步骤,出错后会立即退出
当后面执行的程序需要读取这个变量,或者子 shell 中需要使用这个变量
当你在 .zshrc 里声明了某个变量,例如 http_proxy。那么在 zsh 里执行所有命令的时候都可以读取到这个环境变量。
如果执行某个特定命令的时候临时不想让它读取到这个环境变量,可以这么做:
env -u http_proxy sh -c 'echo $http_proxy'
用 env -u 变量名称
隐藏对应的环境变量,将你想执行的命令放在 sh -c
参数里即可。
如果要隐藏多个变量,可以多次使用 -u 参数:
env -u http_proxy -u https_proxy sh -c 'env | grep http'
env -u
和 unset 命令的区别是,它不会删除全局 shell 环境的变量。
要获得最佳阅读体验,请访问原文 https://baiyun.me/linux-tips]]>
浏览器发出请求,服务器收到后渲染出完整的 HTML 页面返回给浏览器,在这个阶段里大部分业务逻辑都包含在服务端端代码里,如果想要获取新的页面内容,需要重新发送请求,服务器再渲染出完整的页面返回给浏览器,即使实际改变的内容只有一个字也需要服务器重新渲染完整的页面。
这种模式最大的缺点在于客户端无法在不刷新页面的情况下动态更改页面内容,可以很明显看出性能的巨大浪费,以及用户体验的差劲
在这个阶段,借助浏览器提供的 XMLHttpRequest
对象,可以实现在不刷新页面的情况下向服务器请求新的数据,然后使用 JS 动态渲染到页面中,但是点击链接的时候还是需要刷新整个页面,无法做到像原生 APP 一样的体验。
这个阶段和之前的大不相同,浏览器请求页面只会获得一个静态的 HTML 入口文件,例如:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="root"></div>
<script src="/assets/js/app.js"></script>
</body>
</html>
页面中的实际内容完全由浏览器里的 JS 渲染出来,客户端会维护自己的路由、控制器和模型,与服务器之间完全通过 API 来交流数据,此时服务端代码不再处理视图层相关逻辑,可以专注于数据。
整个流程通常是这样的:
这种模式可以在切换页面时避免刷新整个页面,做到类似原生 APP 的用户体验,但是也有两个缺点:
对于 SEO 不友好的问题,有一种解决方法是对爬虫返回使用 Headless 浏览器预渲染页面,这种方法需要额外维护预渲染相关的逻辑,投入回报是个问题
对于首屏呈现时间慢的问题,通常是在 HTML 入口页面中返回首屏需要的初始数据,例如:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="root"></div>
<script>
window.__INITIAL_STATE__ = { data: '...' }
</script>
<script src="/assets/js/app.js"></script>
</body>
</html>
JS 可以直接读取 window.__INITIAL_STATE__
中的初始数据,这样可以省下通过 Ajax 获取首屏数据的时间,效果比较明显,但是在 JS 代码下载、parsing,执行之前用户还是会看到白屏或者加载动画,而 JS 代码的下载和 parsing 又是比较耗时的,所以在移动网络下这个问题会尤其明显。
所谓同构就是指,JS 代码可以同时运行在服务端和浏览器,凭借虚拟 DOM,现在服务器也可以渲染出 HTML 字符串了,例如 React
或者 Vue
这些使用了虚拟 DOM 的库和框架,它们可以运行的服务端和浏览器,同样的代码在浏览器中渲染出真实 DOM,在服务器中渲染出 HTML 字符串。
如果一个 SPA 使用 React
或者 Vue
构建而成,那么它可以很容易转换为同构代码,在同构代码中通常有以下几个注意点:
隔离环境特定代码
例如在 Node 环境中没有window
和 document
之类的仅存在于浏览器环境的对象,所以在我们写同构代码时要注意尽量避免副作用,例如:
// moduleA.js
const list = document.querySelectorAll('ul li')
export default list
上面的代码就不能称为同构代码,它只能在浏览器环境运行,如果想让它变为同构代码通常有两种办法:
// moduleA.js
const getList = () => document.querySelectorAll('ul li')
export default getList
componentDidMount
中执行class App extends React.Component {
state = { list: [] }
componentDidMount() {
this.setState({ list: document.querySelectorAll('ul li') })
}
}
共享状态
在浏览器环境很多状态天然就是单独的,但在服务端它们就是共享状态了,如果我们有一个 API 可以获取用户收藏帖子,如下:
import axios from 'axios'
const getFavoritePosts = () => axios('/api/posts').then((res) => res.data)
export default getPosts
上面的代码在浏览器中使用是没有问题的,浏览器会自动发送用户的 cookie 给服务器,但是在服务器环境中需要将用户的 cookie 手动传给 API 服务器,我们修改代码为如下:
import axios from 'axios'
const getFavoritePosts = (axiosInstance) => {
const ins = axiosInstance || axios
return ins('/api/posts').then((res) => res.data)
}
export default getPosts
此时,我们在服务端调用 API 时需要传入 axios 实例
import getFavoritePosts from './api'
class IndexPage extends React.Component {
static getInitialProps({ axios }) {
return getFavoritePosts(axios)
}
render() {
return this.props.posts.map((post) => (
<h3 key={post.id}>
<a href={post.url}>{post.title}</a>
</h3>
))
}
}
这种做法固然解决了问题,但是每次使用比较繁琐,更好的办法是创建一个通用的 http 请求函数,由它负责动态适配当前运行环境,同时服务端的每个请求都需要携带当前请求的 cookie 和必要的 header 信息。
function request(requestConfig, req) {
if (isServer) {
// 从 req 读取 cookie 和 header
} else {
// 浏览器环境
}
}
class IndexPage extends React.Component {
static getInitialProps({ req }) {
return request({ url: '/api/xxx' }, req)
}
render() {
return this.props.posts.map((post) => (
<h3 key={post.id}>
<a href={post.url}>{post.title}</a>
</h3>
))
}
}
要获得最佳阅读体验,请访问原文 https://baiyun.me/web-development-from-mvc-to-isomorphic]]>
Module Resolution
规则这样的,如果 path 中包含扩展名则直接打包,否则按照 resolve.extensions
中的文件扩展来匹配出最终文件 ,如果是目录则根据 resolve.mainFields
来找到匹配的文件。
resolve.extensions 和 resolve.mainFields 中的内容都是按顺序来匹配的,只取第一个匹配到的值
适合不能用 webpack Module Resolution
特性的情况下使用,这种做法可能导致目录多一层嵌套,但是胜在高效、直观、扩展性好
const dynamicLoad = (scene, subScene) => {
subScene = subScene ? `${subScene}/` : ''
return asyncComponent({
resolve: () => import(/* webpackChunkName: "page." */ `./scenes/${scene}/${subScene}index.js`),
ErrorComponent: InternalError,
})
}
使用 webpack 的 Module Resolution
特性来简化,但是这种做法因为末尾没有文件后缀,导致 webpack 会尝试匹配所有类型的文件,包括那些只是位于搜索路径之下但是没有实际使用的文件,从而造成一定的性能浪费
const dynamicLoad = (scene, subScene = 'index') => {
return asyncComponent({
resolve: () => import(/* webpackChunkName: "page." */ `./scenes/${scene}/${subScene}`),
ErrorComponent: InternalError,
})
}
可以使用 webpack 新增的两个魔法注释 (webpackInclude 和 webpackExclude)来限定匹配范围
import(
/* webpackInclude: /\.js$/ */
/* webpackExclude: /\.noimport\.js$/ */
`./templates/${templateName}`
)
要获得最佳阅读体验,请访问原文 https://baiyun.me/webpack-dynamic-import]]>
JavaScript 引擎是单线程的,这就意味着同一时间引擎自身只能做一件事,如果有很多事情要做,就必须一件一件来,在 JavaScript 中如果前面的某个任务需要耗费很长时间,后面的任务就被阻塞了,同时也无法响应用户的操作,例如 click 事件,看起来就像浏览器卡住了。
如果我们在浏览器中要通过 API 向服务器请求数据,就需要使用 XMLHttpRequest 对象发送一个 Ajax 请求,同时还会监听 HTTP 响应事件并指定一个回调函数来拿到这个数据,如果这个请求是同步的,那么在收到 HTTP 响应之前 JS 引擎就会一直处于阻塞状态,无法执行后面的代码也无法响应用户的交互操作。
如果是异步的就不会通过阻塞 JS 引擎的方式来等待 HTTP 响应,JS 引擎会告诉宿主环境(浏览器或者 Node)在收到 HTTP 响应之后将回调函数插入事件循环队列的末尾,然后自己会继续执行后面的代码,在未来的某个时间点,宿主环境收到这个 HTTP 响应之后就会将回调函数插入事件循环队列的末尾,在事件循环队列里的回调函数最终都会被 JS 引擎按顺序一一执行。
在 You Don't Know JS 中有一段代码可以很形象的表现出事件循环的基本模型
// `eventLoop` is an array that acts as a queue (first-in, first-out)
var eventLoop = []
var event
// keep going "forever"
while (true) {
// perform a "tick"
if (eventLoop.length > 0) {
// get the next event in the queue
event = eventLoop.shift()
// now, execute the next event
try {
event()
} catch (err) {
reportError(err)
}
}
}
可以这么理解,JS 引擎在执行完同步代码之后(或者说 call stack 变空后)就会执行上面这个 while 死循环,程序初始化之后触发的所有操作都会完成后将对应的回调函数 push 到事件循环队列里,JS 引擎会一个接一个按顺序取出队列里的回调函数执行它。
通常有以下几种情况会触发异步操作:
最简单的一个例子:
setTimeout(() => console.log(2), 0)
console.log(1)
第一行表示立即将 setTimeout 的第一个参数添加到任务队列末尾,接着执行第二行, 最后才会执行任务队列中的任务,最终会打印出 1 2
需要注意的是,setTimeout 和 setInterval 在执行时间上是不可靠的,setTimeout 表示在指定的延迟后将第一个参数添加到任务队列末尾,setInterval 表示每隔指定的时间就将第一个参数添加到任务队列末尾。
setTimeout(() => console.log(2), 0)
task() // 假设这个函数会耗时 10 秒
上面的代码在开始执行时就会立即将 setTimeout 的第一个参数添加的任务队列末尾,但是在 10 秒后才会打印出 0
setInterval(() => console.log(new Date()), 2000) // 本意表示每隔两秒打印一次当前时间
task() // 假设这个函数会耗时 10 秒
上面的代码会在 10 秒后连续 5 次打印出当前时间, 原因就在于每隔指定的时间 setInterval 就会不分青红皂白无脑将第一个参数添加到任务队列末尾,等到 javascript 主线程空闲了开始取出队列中的任务执行时也是无脑的,只要队列中还有任务它就会一个接一个的取出来执行,所以传给 setInterval 的函数也是无法保证执行时间的。
如果你需要使用 setTimeout 实现一个动画,可以使用 requestAnimationFrame 代替,如果要兼容不支持 requestAnimationFrame 的浏览器,可以使用浏览器特性检测区别对待。
在浏览器环境下异步任务基本分为以下两种类型:
考虑如下代码
setTimeout(() => console.log(0), 0) // 注意这里
const promise = new Promise(function(resolve, reject) {
console.log(1)
resolve()
})
promise.then(() => console.log(2))
console.log(3)
Promise
的执行器会在 Promise
创建时立即执行,所以会首先打印出 1,执行器的 resolve
和 reject
函数都是异步操作,传给 promise.then
的回调函数会在执行器内部的 resolve
函数执行后执行,如果上面的代码去掉第一行,那么最终会依次输出 1 3 2
如果没有去掉第一行,就会输出 1 3 2 0
上面代码中 Promise 的执行器内部的 resolve 和 reject 函数虽然是异步操作,但是它们并不会添加到 macrotasks,而是添加到 microtasks,它会在每次 Event Loop 迭代结束之前全部执行完毕,所以等主线程空下来的时候会先执行完 microtasks 中的所有任务才会开始执行 macrotasks 中的任务。
例如 Promise 的 onRejected 和 onFulfilled 回调函数就是微任务,其他还有 window.queueMicrotask
传给 requestAnimationFrame
的回调函数浏览器会在下一次重绘之前调用
常见的有 window.setTimeout
以及各种 DOM 交互事件,例如点击和滚动事件。
微任务 > requestAnimationFrame > 宏任务
执行以下代码
setTimeout(() => {
console.log(3)
}, 0)
Promise.resolve().then(() => {
console.log(2)
})
console.log(1)
上面的代码最终输出:
1
2
3
要获得最佳阅读体验,请访问原文 https://baiyun.me/javascript-asynchronous]]>
这是最初版本,传统的 MVC 模式,统计了一下大概 2000 行代码实现了前端到后端的 CRUD
这个阶段的代码是非常简单清晰的,对于一个简单的应用来说也可以了,虽然它并不容易做组件化和工程化。
17年开始,我刚好在当时的公司推动前后端分离,当时花很多精力做了一套 React 服务端渲染框架来支持前后端分离,成功把手头的两个项目从 MVC(Laravel + jQuery + React) 迁移到了 React SSR。
有了这次在生产环境的成功案例,加上工作上写了快两年 React,这个时候再来看 Handlebars.js 这种模板语法就很难接受了(由俭入奢易,由奢入俭难)
并且在这个时候我切身体验到了 MongoDB 的一些体验性问题,比如说,相比 PostgreSQL 和 MySQL 它并没有一个我个人觉得很好用的 GUI,整体周边生态感觉要比关系型数据库落后不少。
基于上述背景,我决定将博客系统的技术架构调整为这样:
整个改造过程大慨用了一个周末加几个晚上,这次重写博客系统的时候顺便单独写了一套 React SSR 工具链 发布在 NPM 上,方便我其他的 Side Project 复用这套工具链。
改造好之后,给我最大的感受就是从农业时代的刀耕火种进化到了工业时代,同样的功能用不同的技术方案去做,开发效率和体验是有非常大差距的。
想切换到 GraphQL 的主要导火索是因为发现了 REST API 在字段裁剪和聚合方面的不便之处,抱着爱折腾的态度,入坑了 GraphQL,此时主要新增了以下技术栈:
graphql-yoga 使用下来也发现了个别蛋疼的问题(比如默认 host 是 0.0.0.0 用户不能更改,为了安全在生产环境还得自己用 iptables 关掉对应端口的公网访问权限),并且相关的 PR 在 github 上一直没有被通过,后续准备替换掉它。
React-Apollo 使用久了也发现不少问题(它会增加不必要的复杂性,对 React 新版本适配也不够快),最终在 2020 年的时候删掉了它,替换为了自己的方案。
前端迁移到了 Next.js 13。主要是眼馋 Next.js 那一套丝滑+开箱即用的构建工具链(turborepo、swc、还有后续的 turbopack)。加上自己没有太多精力持续维护自己写的那套同构框架了。
目前本站是基于 Next.js 13 最新的实验性 Server Component 架构来实现的,整体上比之前代码简洁了一些,性能方面和之前的版本大部分情况下相差不大。但是在网络延迟大的情况下 Next.js 13 的 prefetch 性能和 Loading 状态管理比我之前那套框架差太多了。
甚至 Next.js 13 的 Loading 目前都是有 bug 的,所以本站目前没有启用 Next.js 默认的 Loading。
其他方面 Next.js 13 的缓存微操能力几乎等于没有,这方面导致用户体验比之前自己写的框架差了不少。
最后,没有完美的框架,有突出的地方,就有不足的地方,而 Next.js 框架的侧重点最近几年一直在往静态化和 Serverless 方向发展,所以势必会有不少功能是很多用户不需要的。
要获得最佳阅读体验,请访问原文 https://baiyun.me/my-first-blog]]>