Normal view

There are new articles available, click to refresh the page.
Before yesterdayMain stream

跨境汇款神器:熊猫速汇

11 September 2024 at 00:00

上次介绍跨境汇款服务Wise后,不少国内朋友向我反馈,国内用户无法通过Wise汇出人民币……由于我是用UK信息注册,所以没有遇到这个政策问题。

对于这种特殊情况,国内朋友完全可以使用熊猫速汇(支付宝、微信的国际汇款合作商之一),专门解决人民币汇出中国大陆和其它货币汇入中国大陆的难题

注册大陆地区熊猫速汇一般都是送10元汇全球手续费折扣券,熊猫速汇给了土木坛子一些额外优惠:

  • 人民币汇出“随机派发10-30人民币手续费优惠券,限时:首笔汇款日本或新加坡的客户,由土木坛子联系渠道部的工作人员领取30元人民币优惠券”;
  • 港币回国“首笔免手续费,支付宝汇率提升券,内部会员汇率提升券(限时无次数),限时免手续费活动,8.88现金红包”;
  • 新币回国“首次免除手续费,支付宝、微信汇率提升券,不限时内部会员免除手续费,每个月不定期派发汇率提升券,8.88现金红包”;
  • 英镑/欧元回国“首笔免手续费,闪付收费4‰,老用户8‰,支付宝汇率提升券、微信汇率提升券”。

人民币汇出为什么有安全合规的保障?

熊猫速汇“Panda Remit”是一家专注于提供快速、安全和便捷的国际汇款解决方案的跨境汇款服务提供商。他们致力于为个人和企业客户提供高效、透明和经济实惠的跨境支付服务,以助用户轻松完成国际汇款。该服务支持资金出国和回国,操作简单,费用透明,具有诸多优势。

熊猫速汇是通过与天津金城银行合作来实现汇款,天津金城银行股份有限公司成立于2015年4月,是银保监会批准成立的全国首批五家民营银行之一。金城速汇产品(汇出中国服务),汇款人信息均由金城银行依法合规采集,且由金城银行提供中国境内全部相关汇出汇款服务,熊猫速汇仅向金城银行提供境外收款人清分服务及向境外收款人提供汇兑服务。

当前熊猫速汇人民币汇出服务汇款单笔限额最低为人民币100元,最高为人民币49000元; 根据熊猫速汇合作银行规定与授权,给予每人每年最高30万元人民币汇款额度。

中国大陆人民币资金汇往国外熊猫速汇优惠注册

中国大陆人民币资金汇往国外熊猫速汇优惠注册链接,或者直接扫描上方二维码。

注意,一个内地手机号码可以同时注册: 大陆、香港、新加坡等其他各个地区熊猫速汇的账户。比如:你可以使用同一个手机APP切换不同的地区登录,然后可以使用大陆账号从大陆汇款到新加坡、香港、英国,也可以使用香港账号汇款到大陆、新加坡、日本,或者使用新加坡账号汇款到澳洲、美国,随心所欲、一个账户汇往全球。

境外资金汇往中国大陆熊猫速汇优惠注册

境外资金汇往中国大陆熊猫速汇优惠注册链接,或者直接扫描上方二维码。

注册流程:

  1. 注册账户:使用邀请链接注册,享受优惠。
  2. 实名认证:使用正确证件进行认证。
  3. 添加收款信息:填写收款人详细信息。
  4. 确认汇款:提交申请并完成付款。
  5. 确认到账:通常一个小时内到账。

熊猫速汇优点

  • 低成本:手续费仅为80元人民币,相较传统银行可大幅节省。
  • 快速到账:通常一小时内到账,效率高。
  • 无外汇额度占用:人民币汇款不占用外汇额度,但每人每年最高不超过30万人民币
  • 优惠汇率:提供优惠汇率。
  • 操作简单:注册到汇款流程简单,支持在线实名认证。
  • 安全保证:持有金融牌照并接受监管,确保资金安全。
  • 汇款全球:支持40多个国家/地区的汇款,包括iFast英国、OCBC新加坡、Wise等。
  • 费用透明:费用清晰可见。
  • 无需换汇:可直接付款人民币,避免限额问题。

熊猫速汇缺点

  • 非同名汇款,但合法资金来源并提供汇款证明即可。
  • 不同地区汇款需单独实名,实名证件要求不同。
  • 不提供个人IBAN,到账显示的非个人名称,不适合像OCBC的首次同名入金验证需求。

欢迎大家扫码下面二维码加入微信群共同交流汇款经验、技巧和问题(或者添加微信 tumuhk, 请注明“熊猫”),并领取熊猫速汇给通过我的优惠注册链接不定时发布的内部福利(熊猫速汇工作人员也在群内)。
熊猫速汇内部汇款福利群

如何把钱从新加坡转到香港?Wise使用体验

30 August 2024 at 00:00

money

以前我在英国和欧洲时,都是直接到银行操作海外跨境汇款,直到最近几年见JUSTYY博主给我汇款时使用Wise,但我没有这个需求,所以注册了这个Wise账号后也没有亲自试验。

关于Wise

Wise的前身是一家2011年成立的英国金融科技公司,前称TransferWise,是伦敦金融监管局授权的电子货币机构,是伦敦证券交易所上市公司,为全球大多数国家提供国际汇款服务。
Wise提供服务包括国际汇款、借记卡、Wise账户等,中国商户和个人可使用国际汇款和Wise账户汇款、换汇等,外汇可以以极低的汇损提现到国内银联卡和支付宝,或者汇款到国外。中国大陆账户不支持实体卡。

最近因为我想把我新加坡OCBC银行(想开户的朋友参考新加坡OCBC华侨银行线上开户完全指南)上面的一些资金汇到香港的银行账户(想开户的朋友参考香港银行开户经验),然后再通过中银香港和内地之间的免费汇款服务汇到内地,于是体验了一下Wise的国际汇款服务。

作为体验,我尝试汇款3200新加坡元SGD到香港并变成港币HKD,Wise的汇率是1 SGD=5.9604 HKD,扣除Wise的整体汇款费是10.12 SGD(60元左右)后,香港的银行收到了19013.09 HKD。

而如果我直接通过OCBC银行的国际汇款服务,手续费每笔35美元等值,邮费每笔17美元等值,电报费每笔35美元等值,加起至少八九十美元了,因此平均无论是汇率还是手续费,我感觉Wise都不错,Wise的手续费率才千分之三左右。

速度方面,Wise几乎是秒到账,这比银行的跨境国际汇款到账速度快多了——我上次通过国内的工商银行汇款到OCBC也花了几个小时才到账(手续费80元左右),Wise在到账速度方面几乎无可挑剔。

不试不知道,一试吓一跳,原来Wise的服务如此好用。这其实就是专业化,Wise是专门做转账汇兑跨境汇款服务的公司,比起银行自身的国际汇款服务,自然要强得多,就像国内的支付宝做支付,在日常生活中其实已经干翻了各银行的银联支付。

Wise虽是第三方公司,但它也有正规的金融牌照,我通过它申请到了各主流国家银行的银行账号(没有实体卡),Wise甚至还送给了一个VISA的虚拟银行卡号(Debit Card,我使用了非内地信息注册)。

新加坡和香港都没有外汇管制,因此两地之间的资金通过Wise可以自由低成本地流动,当然前提是资金来源合法合规。

各位朋友,趁着国内也还能注册Wise,可以注册一个账号使用,或者留着未来备用。

通过我的Wise注册链接注册,可以获得最高¥4,500等值的免手续费汇款优惠。注册使用其实很简单,通过链接下载APP或者直接在网页上按步骤注册使用即可。

欢迎大家也分享国际跨境汇款这方面的优惠信息和实战经验,让资金自由方便地流动起来。

手把手教你长桥证券开户入金教程【港美股永久免佣】

3 August 2024 at 00:00

投资有风险,入市需谨慎!

总有朋友问我如何开户美股券商账户?之前觉得这种简单的事情不需要单独撰文,看来是我错估了。下面我以长桥券商为例,重点介绍一下长桥的开户步骤流程——其它的券商的开户方法也大同小异。

先送福利

承蒙厚爱,长桥官方给土木坛子博客读者配置了一个渠道专属注册优惠,推荐码是:783XQW,专门的注册开户网页链接:

香港长桥:https://app.longbridgehk.com/ac/oa?account_channel=lb&channel=HB100006&invite-code=783XQW

新加坡长桥(可买数字加密货币ETF):https://activity.lbmkt.ing/pages/longbridge/7415/index.html?appid=longbridge&orgid=1&account_channel=lb&lang=zh-CN&channel=HB100006&invite-code=783XQW

渠道专属活动,注册后(注册时填写土木坛子渠道邀请码:783XQW)通常每个月都有丰厚福利奖励(长桥新加坡账号入资对应等值新币,奖励相应免佣等福利)。

长桥开户流程

长桥开户流程

下载长桥APP(应用市场搜索长桥即可,APP不分香港和新加坡,账户注册有分别),有内地或者港澳的手机号都可以注册,填写邀请码 783XQW 后,便可注册成功。至此你已经可以使用长桥APP查看行情、资讯了。

长桥开户流程

按照中国证监会要求,现在已不再允许境外券商平台给中国境内用户新开户,所以券商平台都会要求用户提交存量投资者证明。长桥的要求:

  • 海外券商账号账户证明结单;
  • 香港银行开户证明或者结单。

此时你可以直接提供相关银行的账单(中银香港和汇丰APP里都可以下载)。

长桥开户流程

这里还有一个简化的流程,如果你在境外,或者设备IP是香港和澳门的话,可以跳过存量投资者验证这个流程,只需要提供身份证即可。大家可以人在香港或者澳门的时候,直接在那边手机开户,或者用点魔法手段(可私信土木坛子微信 tumuhk ),让手机暂时变成境外IP也可。

长桥开户流程

后续流程按照要求提交相关资料,以及投资经验、风险承受能力的调查问卷,就可以完成开户了。

长桥开户流程

如果是在工作日申请的话审核通常会很快。

长桥的入金流程

开户成功之后,为了达标获得开户奖励,接下来就是入金了。

长桥开户流程

长桥支持的入金渠道挺多,在资产页面的存入资金即可进入入金流程。

长桥开户流程

由于长桥自己的港币>美元兑换汇率比银行要优惠点,我一般是直接港币入金,选择港币后,可以选择香港地区的银行,以汇丰香港为例,支持eDDAFPS转数快网银转账ATM/柜台支票等渠道。

其中的eDDAFPS转数快是最快的渠道,基本上秒到账。

在这里注意一下,汇丰的eDDA授权中,银行开户证件类型需要选择港澳通行证(除非你当初开户的时候用的是护照),如果填错了也没关系,银行那边会拒绝授权,重新申请即可,注意银行短信或者邮件提醒。

长桥开户流程

第一次入金,建议可以先小额试一下,看看自己的银行渠道入金是否正常。成功之后,长桥APP和银行方都会发送相关通知。

至此,你的券商账户就已经开通了,接下来就可以按照自己的需要,进行港股和美股的投资了。

开户之后,注意留意下开户奖励,在你入金达标后,例如免佣卡股票卡会陆续发送到你的账号,可以在我的>奖励记录中查询。

学习、交流、分享

如同常言道:投资有风险,入市需谨慎!

美股投资风险和收益共存,对于新手来说,甚至风险大于收益。对于所有加入这个领域的人来说,都需要有足够的认知和心理准备。

我自己也偶尔会分享一些自己的学习心得,期待与大家一起交流和学习。

祝大家投资赚钱愉快顺利!

本教程取自罗磊的独立博客,特此感谢!


PS 你如果实在还有疑问(包括但不限于境外银行开户、券商账户开户等),可扫描二维码添加我的微信号( tumuhk ),请注明“美股”,我尽力解答你的相关问题。

土木坛子

比特币ETF是什么?怎么参与交易?

2 August 2024 at 00:00

投资有风险,入市需谨慎!

Bitcoin ETF

经常有人问我什么是现货比特币ETF?如何参与购买?比特币已经让很多人产生疑惑了,今年市场上又推出好多比特币现货ETF(还有以太坊ETF),似乎把问题变得更加复杂了。

实物黄金和纸黄金

我们不妨拿现货黄金和现货黄金ETF来比较(之所以强调是现货,意思它不是期货,确实早就有期货ETF了)。

假如你要投资黄金,你当然可以去银行或金店买金灿灿的黄金实物,代价是你购买的时候需要自己鉴别黄金的成色,买回来放家里自己保管或者存放在银行的保险柜(需要租金),另外你似乎拿100块钱也不方便买,怎么着也得买个1克以上吧?卖的时候同样有些不方便。

但是,投资机构根据国家法律搞出来现货黄金ETF,你不必买实物黄金,而是直接买ETF份额,比如你买100元某公司发行的现货黄金ETF即可,该机构根据这100元按现时价格去购买相应的黄金实物替你保管。卖的时候照样,卖出ETF即可获得现金。

这样,未来黄金的涨跌,你通过持有相应份额黄金ETF来享受投资黄金的收益,黄金价格涨,ETF也按相应比例的上涨,黄金价格下跌,ETF也按相应比例下跌。

聪明的你看出来了,这个所谓的现货黄金ETF,其实就是“纸黄金”,你购买的只是一个权益、一个份额,你自己并不真拥有黄金——而机构代持了,但你能和直接购买黄金一样享受投资黄金的收益。

拥有ETF的好处显而易见,你省去了鉴别、保管黄金的麻烦,你的代价是给这个机构支付一定的管理费用和交易费用。当然,你得相信这个发行ETF的机构——它们受国家法律监管。

以此类推理解比特币ETF

现在你把“实物黄金”换成“比特币”,把“现货黄金ETF”换成“现货比特币ETF”,大概就能明白现货比特币ETF是什么了。

简单说,比特币ETF是一种交易所交易基金,它允许投资者通过传统证券交易所参与比特币市场,而无需直接购买和持有比特币,也就是购买比特币ETF可以相当于持有一部分数字货币。这种投资工具为希望投资比特币但又担心加密货币交易所操作复杂性和安全问题的投资者提供了便利。

那为什么要持有比特币ETF呢?这是因为对大部分普通人来说,购买和持有比特币这个东西比购买持有黄金更麻烦一些。我们听到太多比特币被盗的事情了,实际上并不是比特币本身的问题,而是人们缺乏相关的知识和技能,难以驾驭虚拟电子世界里的加密资产。

拜华尔街的力量,今年美国和香港今年推出现货比特币ETF(后来还有现货以太坊ETF)后,这一切就方便多了。想要参与投资比特币的人们,尤其是上了年纪或不懂技术的年轻人,参与就简单多了,直接购买相应投资机构发行的比特币ETF即可。买卖交易比特币ETF就和普通股票没有任何区别,和交易股票一样——就在券商系统里交易,且受法律保护,买卖时有一点交易费用,另外有的ETF发行机构可能会收一点管理费,这些是人家的利润来源。

方便技术小白们

通过比特币ETF的形式,一下子把加密数字资产世界和传统证券交易市场连通了,那些传统的老钱(意即传统人士和机构)都能参与到这个市场来——美国一些养老基金都已开始配置比特币ETF了。不懂技术的人们终于可以绕开比特币那些难以理解的概念和技术(比如私钥、冷钱包、非对称加密、区块链等),可以不直接拥有它而获得它的涨跌收益。

通过比特币ETF获得的收益合法、合规,没有币圈出金(尤其是某些国家)面临的那些麻烦,因为卖掉比特币ETF得到的钱来自证券所交易,无比干净,且是税后收入。

当然,对于加密圈资深高手来说,我认为他们还是愿意直接购买持有现货比特币,因为手续费成本更低,7×24小时全天候的交易模式也更自由,变现成任何法币也不是难事。

不在境外的人如何参与?

对于某些国家的人来说,直接买卖现货比特币本身有难度,参与比特币ETF也有一定难度,好在目前我已经替大家研究出来了特殊的管道路径,说起来也不复杂,就是在线办理新加坡的OCBC银行卡,汇款到银行账户,然后开通新加坡长桥证券账号(香港券商账号不对内地客户开放比特币ETF相关资产),之后就可以把银行的资金转到长桥证券账户上买卖美国(也含香港比特币ETF)的比特币ETF(美股也行)等资产标的了。

机会往往存在于很多人还不了解知晓的时分,等大家都知道了,往往就不是机会了。11年前,我认为比特币是一个新金融时代的开始,我一师弟投了一点钱进去,持有这十多年来的复合年化回报率达到了近60%(即涨了100多倍)。后来由于相关政策,近年来我很少再直接谈论。今年香港和美国比特币ETF出来后,我认为这极大可能又是一个机会。更多资金冲进来,而由于比特币限量的原因,长期涨势有保证。

勇敢的人先享受世界。作为记录,截图为证——截图是今天美国和香港的部分现货比特币ETF价格(绝非推荐它们),让时间来说明一切。

ETF price

PS 你如果实在还有疑问(甚至包括境外银行开户、券商账户开户等),可扫描二维码添加我的微信号( tumuhk ),请注明“ETF”,我尽力解答你的相关问题。

tumuhk


承蒙厚爱,长桥官方给土木坛子博客读者配置了一个渠道专属注册优惠,推荐码是:783XQW,专门的注册开户链接:

香港长桥:https://app.longbridgehk.com/ac/oa?account_channel=lb&channel=HB100006&invite-code=783XQW

新加坡长桥:https://activity.lbmkt.ing/pages/longbridge/7415/index.html?appid=longbridge&orgid=1&account_channel=lb&lang=zh-CN&channel=HB100006&invite-code=783XQW

渠道专属活动,注册后(注册时填写土木坛子渠道邀请码:783XQW)有丰厚福利奖励(长桥新加坡账号入资对应等值新币,奖励相应免佣等福利)。

附送:手把手教你长桥证券开户入金教程【港美股永久免佣】

10条永恒的投资原则

1 August 2024 at 00:00

有网友说:学会投资是现代人必备的技能,如同阅读、写作、吃饭、睡觉一样重要。我深表认同,刚好国外推主Brian Feroldi分享了10条永恒的投资原则,我结合自己的经验对这些原则作适当解释,在这里分享给朋友们。

投资

1:如果你想积累财富,你必须投资。

靠一份工资对于绝大部分人都是不够的,哪怕打工皇帝都未必能仅靠工资实现财富自由。

投资

2:在你准备好之前,不要投资股票。

首先关注财务健康。

不打无准备之仗,没有任何基础就大手笔投入资本市场,会亏得很惨。学习投资的基本原则和风险控制是有必要的。

投资

3:当你的个人财务非常保守时,应对波动性会容易得多。

在风险较高的市场,要采取较保守的思路,而不是继续加杠杆,控制风险保住本金,活得久才是王道。

投资

4:在一开始,你的储蓄率是最重要的。

随着时间的推移,你的投资回报将变得最重要。

滚雪球需要先有一个小小的雪球才能启动。刚开始还是要有一定的本金才能参与——初期努力储蓄本金吧,时间的积累后期的回报率如果也高,投资的整体回报才能高,才能远超工资储蓄带来的整体收入。

投资

5:短期内有风险的,长期内是安全的。

短期内安全的,长期内是有风险的。

比如存款,短期内是安全的,但长期来讲,货币贬值带来的风险几乎是确定的。长期安全的东西,可能在短期内是波动的,因为它倍受关注,投机分子在短期内会搅动市场。

投资

6:平均成本法使市场时机变得无关紧要。

这里的平均成本法,其实就是我一直推崇的定投方法,只要投资标的长期是上涨,就可以无视短期内的波动,大胆采用定投的方法。

投资

7:短期内,企业及其股票的相关性为0%,但长期内为100%。

短期内的波动与企业(或者投资标的基本面)业绩关系不大,但长期来讲,只有基本面才能决定它的价格。这也就是上面提到的对这类标的采用定投即可。

投资

8:人类天生不擅长投资。

要明白,你的情绪会对你玩弄各种花招。

人性是不可靠的,很容易情绪化,逢低买入,逢高卖出,这么简单的道理,但绝大部分人一操作就成了:追涨杀跌。涨了就FOMO,怕错失机会,跌了怕亏更多,赶紧止损卖出……

投资

9:你不可能什么都知道。

定义何时你了解足够的信息来做出决定。

我们凡人的能力、精力和时间有限,不可能是全知。深耕某一两个赛道,做对这一两个赛道,足矣。

投资

10:拉远视角。

就像上面所说,对于一个长期来讲是上涨的标的,当短期内的波动扰乱心绪时,拉远视角,以3、5年的时间窗口来看,就知道它是上涨的,不必被短期波动所困扰。做时间的朋友。

鸣谢,本文图片取自Brian Feroldi

解决出金刚需,VCard支付宝转账功能上线体验

31 July 2024 at 00:00

VCard注册安装推荐链接(邀请码110316):https://webapp.51vcard.com/#InviteRegisterPage?inviteCode=110316

之前介绍的VCard虚拟银行卡最近推出一个新服务,转账美元到支付宝变成人民币

官方说:

业内首家开通支付宝转账功能,无缝对接支付宝,解决一切资金安全回国问题!通过VCard转账到支付宝,无需繁琐的步骤,可以将VCard钱包余额直接转入您的支付宝账户。

简单来说,就是通过VCard可以把USD汇入自己国内的支付宝账户,支付宝账户里通过跨境闪速汇款自动接收变成人民币。

上周,VCard的官方妹子就告诉我即将推出这个功能。昨天终于正式推出,我于是第一时间开通这个功能亲身体验。

开通费用50美元,好在只是一次性费用。开启后我在APP上简单设置了收款人信息——即我的支付宝账号信息,及付款人信息,即我自己,需要上传身份证或护照信息等KYC信息。

VCard

我通过VCard汇出115美元,费用为汇款金额2%的手续费,也即2.3美元,外加5美元额外费用(与金额无关),总共7.3美元。

然后再去支付宝查看,告知到账831.36元人民币,显示通过跨境闪速汇款接收的,接收方不需要任何额外操作,方便快捷。

如此算来,115美元兑换成831.36元人民币,也即汇率7.23,市场中间汇率7.25左右,因此汇率7.23还算可以,实际上汇率是由支付宝的国际汇率决定的,VCard不参与汇率。

值得注意的是,这个汇款并不占用每个人每年的国家给的5万美元外汇额度,但也并不是说汇款额度没有限制,每次最多能汇4000美元,同一个收款人或付款人,每3天限一笔。每个接收人每年限5万美元——可换不同的接收人。

因此,根据我的使用体验,先说优点:

  • 速度真的快,瞬间到账,操作非常方便简单(详细操作步骤)。
  • 不占外汇额度。
  • 资金是通过支付宝汇入,安全合规,不担心收到黑钱冻卡。

再说两个不算缺点的缺点:

  • 开通成本50美元。
  • 费率不算低,从美元到人民币,按4000美元一次,收费85美元,最低费率也达到2.1%。

总体来说,2.1%以上的费率不算低,但也绝不算高——境外银行卡在国内消费也有2-3%的手续费呢,何况VCard提供服务要考虑盈利,因此综合上面提到的优点,我觉得这次VCard新推出的汇款到支付宝服务总体还是不错的,对于没有其它更好选择的朋友来讲,是一个非常好的出金渠道。

说到出金二字,本博国内的不少朋友,应该不缺USD吧,懂的都懂。VCard产品开发很用心,继上次推出VCard实体银行卡支持转账到PayPal后,这次VCard推出转账到支付宝服务,解决了部分朋友出金的刚需痛点,大家就偷着乐吧。

真人 cos AI coser 的 cos 照,不远了。

By: Steven
17 February 2023 at 19:29

摄影把「画得像」逼到了天花板,终结了传统油画,开启了现代艺术。现在能用 AI 把同人图画到真人 Coser 照的程度,基本可以宣告静态图像艺术已经快被逼近天花板了。可以预见未来的一到三个月内,会有一些真人 Coser 或特效化妆的团队开始尝试去 cos 由 AI 生成的仿真 Coser 照。

基于 AI 二次创作基础上的真人再创作,真人和机器的创作界线会发生很有趣的交融。这种灵活机动性和融会贯通的能力,恰恰是人的优势所在,就看谁能更好地拥抱新技术了。

人类历史中新一波的艺术革命会在未来十年间逐步揭开大幕,接下来就看下一个毕加索和塞尚会在什么时间以什么姿态出现了。我还是之前视频里的观点:

未来艺术的主流形态,是动态且可交互的。由人进入场域中的体验来最终完成作品,千人千面的哈姆雷特会成为天龙八部的珍珑棋局。

图像创作者:勘云工造 @auditore_k

图集下载地址:

Arknights-texas

https://drive.google.com/file/Arknights

Password:kanyon

Azur Lane-Cheshire

https://drive.google.com/file/Azur

Password:kanyon

人类的定义正在重构的历史开始了

By: Steven
13 February 2023 at 14:02

上周在即刻看到一个话题:

ChatGPT vs. iPhone 两种技术有何异同? 就它们制造产业变革和影响来说,对比思考能否启发对未来的想象?

我目前的看法是:

iPhone 为代表的技术,拓展了人的外延。人是技术网的中心,是作为生物人抛向空中的一块大腿骨。

ChatGPT 为代表的技术,更新了人的定义。信息和意识不是人的特权,是否只有生物人才是人,需要被认真严肃地对待。

前者的产业革新是建立在人脑上限之中的,超过脑容量的部分,推进速度非常缓慢。

后者不存在理论上限,对自身的推进速度远大于前者。但由于前者高度依赖人这个不确定因素,因此,在后者产生革新的同时,会同时产生大范围的剧烈冲突。前者脆弱,但作为后者的基建,这种大范围冲突可能导致两败俱伤,拖累后者进化速度。但因为发展不平均,所以后者会衍生出全新的社会形态。

AI 不需要代替人才能更新「人」的定义,更不必达到硅基生命的程度,只需要在表达方式上像人(即便它根本不理解自己在说什么),就自然会在生产方式和伦理上产生大量冲击。这些冲击会更新人对自身的认识,配合其自身的效率属性,人会主动更新对自身的定义描述。

在此基础上,设计师将来的工作会和今天大不相同。因为我们在思考人与物的关系时,中间的媒介可能不会再是物理交互和界面交互,而是面向 AI 的交互。这种交互可能是有形的,也可能是无形的。另一种更有可能发生的情况是,你所设计的产品不是给人类使用的,而是面向 AI 的中间件,这会改变很多约定俗成的非物理/生物层面的规则。

我们有幸站在了这段历史的开端之中。

尼卡果实并未令我失望

By: Steven
16 January 2023 at 22:34

问题来源:为什么大多数人不能接受《海贼王》橡胶果实变成尼卡果实?

我的观点:

因为尾田既坚持了热血王道漫的套路,又打破了读者既定的期待和幻觉。

无论你作为个人是否承认,但你无法否认一件事,人们喜欢看热血王道漫的一个基本心理预期是:把自己作为故事的主角代入进去,感受那种拼搏到最后,获得成功的痛快!但这个故事套路在大部分人的心中有一个基本模板,那必然是由下至上的成长与逆袭,因为这是绝对多数读者的真实愿望。人们知晓自己的渺小,但渴望成功,这才造就了这类题材的大热。不接受「尼卡」并非不接受这个名字,而是不接受「自己无法继续代入」,无法继续成为「由无名之辈进阶到封侯拜相的故事主角」。人们所失望的,是「那个王不是自己」。

想必很多人不同意这个观点,但我想提三个问题,请各位一起想一想:

1、艾斯死的时候,岩浆凌驾于火焰之上,可以接受吗?

2、获得了同一颗果实的萨博所打出的炎帝,和艾斯的是一回事吗?

3、香克斯能结束顶上战争,五老星也得给面子,真的是面子果实吗?

我的一些浅见是:

一、多数人并没有因为艾斯的死而质疑果实上下级的关系,最核心的原因在于,几乎没有读者会把自己代入到艾斯的角色里,不会觉得自己的愿望无法达成而失望和愤怒。因此可以比较冷静地从旁分析果实上下级的合理性,去理解这个设定的前因后果,甚至认为尾田这么画是故事推动到此处的必然,是故事的神来之笔。

但其实岩浆是高温融化的石头,火焰是氧化反应的表象,两者根本不在一个维度上,除了温度高低外并没什么关联,但没人会在乎这种类型的 bug,因为它不影响读者的阅读体验,没人会觉得自己是艾斯,给我个差不多的理由就行。

二、如果两个人拿到同一颗果实后,施展出的是不同的招式,那么我们可以说,个人对果实能力的开发至关重要;如果两个人施展出的是同一种招数,只是训练强度不同,造成的效果强弱有差异,那么,个人对果实的开发就不是那么关键的,而是训练的效率更为重要。

回到艾斯和萨博的例子上,这一招炎帝,是属于艾斯的,还是属于果实的?

很显然,既然萨博能够打出和艾斯差不多的炎帝,那就意味着,炎帝不是艾斯的专属,是任何一个拥有这个果实的能力者都可以训练出来的一门技术。那么,作为一门技术,它能否发挥出强大的效果,是取决于果实本身,还是使用果实的人?我相信黑胡子很清楚这个道理,否则他不会那么执着于获得白胡子的果实,更不会坚持要打造最强的全员能力者海贼团。因为他是懂得技术的价值与人的关系的人,不然他干嘛要隐藏那么多年,一心选定暗暗果实呢?尾田把手术果实安排给艾斯的话,合理吗?

再进一步,既然贝加庞克能够复制果实能力,能够把血统因子工程化,那就意味着无论是果实还是种族,在海贼世界的设定里就是科技树的一部分。但凡是科技,是刀杀人还是人杀人的争论就绝对不会停止,但无论怎么争论都必须正视一件事:没人使用的科技,啥也不是。

换句话说,尼卡这门技术可以交给任何人,但最好是路飞。

因为那么强大且令人发笑的能力,最好交给最纯真的那颗心来驾驭。这样的剧情安排才合理,否则这个从一开始就贯穿至今的「Joyboy」到底「Joy」在哪里?让巴基来 Joy 大家吗?一个胆小怕事的真小丑,你觉得能够让大家 Joy 起来吗?还是交给黑胡子这样的终极大恶,最后用橡胶锤打扁他?

我相信 Joy 不是那么肤浅的快乐。

它最好由一个没心没肺、吃人一口饭就念着报恩的傻子来实现。

三、比起路飞的宿命,这个红发男人的宿命就不值得失望和愤怒了吗?

这个男人串起了消失的一百年、月球文明、世界政府的巨型草帽这种超巨大的谜团,这样的人会因为一个村子的小孩吃了个橡胶果实不能游泳就舍弃一条手臂?已经把新世界的希望堵在这种事情上了,哪个脑回路清晰的作者会把解题的重点放在刻苦开发果实能力、卷赢其他能力者这个方向上啊?

大家都是东亚文化圈里熏陶出来的人,尾田会真心觉得卷是路飞的出路吗?香克斯和路飞一出场就把牛吹那么大,一个随意进出世界政府的面子人,咱们平心而论,他罩着的主角要只是个逆袭屌丝,你觉得这样的安排跟玛丽苏/杰克苏有什么区别?

宿命,不是原罪,无力面对宿命才是。

假如后期的剧情,路飞成为尼卡果实的表演者,失去了作为他自己的个性,没有主导和实现自己的想法,那么我会说很失望;如果接下来的路飞依然是那一千多话的没心没肺大笑、快意恩仇不拘小节的大傻子,做判断的依据仍然是他的纯真内心,那他就是尼卡的主人。

很多人觉得自己只要也拥有了一个亿,就也能干出一番伟业来。这是宿命论者们最喜欢相信的谎言,也是被宿命打击时最容易失望和放弃的原因。但事实上不是的,你拿着顶尖的武器,没有使用它的方法和能力,你什么也做不了。

海贼世界里,哪个能力者的果实和他的个性不是对应关系的?这就不是宿命了?这有一颗果实,无论什么属性,是它主宰你,还是你主宰它,这才是宿命与否的唯一判断。

我不认为随着剧情的推进,尾田一点点把世界观和科技树铺陈开,这有什么问题。尤其是当你知道,古代的科技远超今天,此时的科技全靠考古的时候,一切都说得通了。那么果实这种科技产品,作为一项工具被人使用,就一定不会是卷中之卷的通关秘籍。

路飞拿了一手好牌就令人失望么?

不是的,他把好牌打烂了才令人失望!如果没有前面一千多话的历练,那么尼卡果实就是个普通的橡胶球,路飞也不过是个不会游泳的普通人;为什么在打凯多的时候觉醒,恰恰是因为凯皇才配得起这个巨大的份量,只有被三番五次打败后的路飞,才有机会完成历练,让尼卡成为尼卡,让自己成为配得起尼卡的那个人。

路飞正在成为可以直面和挑战宿命的那个人,把自己代入到主角视角的读者,也应该有这份心情和觉悟,才能体会到「命运管不着老子」的热血王道漫之精髓!尾田是真懂的。

香港存款利率高达10%是真的吗?

27 July 2024 at 00:00

money

因内地这些年的存款利率越来越低,而年轻的朋友越来越喜欢存款,于是偶尔有朋友或者文章提到香港那边的存款利率很高,有的高达10%,简直不敢想象。为什么这样的好事我没有碰到?

实际上情况是这样的吗?美国因这些年的加息导致目前存款贷款利率比较高,本文写作的今天,美联储的贷款基准利率大概是5.50%,美国财政部的国债收益率2个月期的年化收益率确实高达5.369%。

香港因为其资本自由,港元与美元采用固定汇率制度:通常1美元兑换7.8港元左右,因此,理论上香港的存款利率高自然就理所当然了。

我在众安、中银香港、汇丰银行的APP上认真查看了一下,存款利率实际上很低,低到不到年化利率1%,比我们内地好不到哪里去。那些鼓吹存款利率达到5%甚至10%的事情是怎么回事?

我研究了一下,发现这种收益率也不是没有,而是众银行的营销策略,也就是说给一个很短期的高利率产品,比如你存一周的周期存款,这一周的年化利率可以给到你10%以上,但由于时间短,其实利息也没有多少。像众安银行经常发利率优惠券,变相把短期利率内加到6%甚至更高。

对于银行来说,这样的高存款利率基本上就是一个营销口号,不明真相的人们把这个信息片面理解成存款利率高达10%,心存鬼胎的自媒体为了流量,更是不断加强传播这种片面的信息。

既然美国国债的收益可以高达5%左右,理论上在香港这样的自由市场也可以获得这么高的年化收益率,只是它就不能是银行存款这样的产品了。

我发现,通过香港银行或者证券商的渠道,可以购买美元或者港元货币基金,收益率基本上能达到年化5%以上,因为它基本上是和美国国债挂钩,所以它的利润来源合理,基金风险也极小,当然,它是基金产品,理论上不能说和存款那样完全没有风险。

比如众安银行APP里就有货币基金购买服务,免佣金,只收一个极低的管理费,美元货币基金是5.5%左右的年化收益率,港元基金也可以,年化利率低一点,接近5%。当然通过像长桥这样的券商购买货币基金费用和收益也类似。

在我看来,这种年头风险极低但年化收益率能高达5%的事情,也算是相当不错了。不过,下半年美国大概率要降息,收益率也大概率会多少下降了。

因此,香港存款利率高达10%到底是真的还是假的?是真也是假,但你至少不会被这种信息带偏了。

投资有风险,入市需谨慎!

赚钱的事情要趁早

25 July 2024 at 00:00

趁早

上午去中国银行一问,才知道我的银行卡被限额每天5000块。要提高限额的话,需要我拿一堆资料申请,银行再看我的资金流水再决定。问题是我因为限额,一直没怎么使用这张银行卡,于是一下子陷入了鸡与蛋的死循环了。气得我直接劝告银行工作人员:你们赶紧找个其它的工作吧,银行业这样子下去,你们是没有希望的。

我其实一直都觉得银行不靠谱感,直接说就是三个字:不信任。前几天查阅资料,居然发现“历史上,咱们曾经发生过四次冻结银行存款的事情”

今天再去搜索这篇文章时,好几个链接已经无法查看,想想都有点不淡定了。看来读文章也得趁早,晚了就看不到了。还好我没什么存款,拥有的是银行负债,贫穷是我的保护伞。

新闻说贷款LPR利率下降了,明年房贷估计能少交一点钱,感谢国家关心我这样的房奴一族。贷款利率下降后,存款利率也得下降,不然银行吃什么?

最新的五年期定期存款年化利率为1.8%,进入了2时代。记得几年前大额存单是3.99%的年化利率。

梦里依稀慈母泪,城头变幻大王旗。看来假如有存款,也得趁早存进去锁定利率收益。还好我没有存款可存,也就没有遗憾了。

近日收到券商长桥通知,马上新加坡长桥开户的条件之一是存量用户,或者就是海外生活工作的人。

何谓存量用户,即2024年6月1日前已经拥有其它海外券商账户才能开通新的券商账户,国人不是存量用户的话那就需要你肉身在境外,比如跑一趟香港。

6月1日前没有在其它境外券商开过户的话,就不能随意再开其它新的新加坡账户了(其它地区未知),看来开户也得趁早。

好消息是,目前还有办法,我似乎找到了系统的BUG,暂时还可以低成本解决上面的开户新规,有需要的有缘人加我微信: tumuhk (注明新加坡),这种信息差只能私聊。

只是不知道这要求国人存量用户开户的背后逻辑是什么,也许资金只能在境内玩?也说不定将来国人完全不能境外开户了,彻底“保护”好大家的资金不被境外资本赚走,肉烂在锅里也好过被别人吃了。

机会的大门不会永远敞开,看来开个玩美股的境外证券账户也得趁早。为什么是新加坡券商?因为新加坡OCBC银行非常方便线上开户,再配合新加坡券商账户,可以自由地玩转美股。

OneKey的BTC闪电钱包试用体验

31 May 2024 at 00:00

BTC

一直有听说BTC闪电钱包,转账十分快速,且费用低廉。但百闻不如一见,亲自体验才能知道,于是找了时间试用了一下OneKey的BTC闪电钱包。

我直接使用上次OneKey官方送我的硬件钱包,设置好Bitcoin钱包后,在电脑端安装好相应软件,生成一个闪电钱包后,便从Binance币安上提现出那可怜的一点点BTC。按照要求,在OneKey软件端填写以sats聪为单位的BTC数量,生成invoice,也就是转账的收款地址(或者叫票据、发票也行),将这个地址复制到币安上,申请提现即可,也可以用币安的APP扫那个invoice二维码,这里币安会收0.000001个BTC,按今天的价格大约是0.5元。

不到几分钟,币安审核通过后,几乎是同时OneKey钱包的软件上就看到到账了。整个过程,确实达到了费用低廉、快如闪电的效果,也许它真的是未来BTC的支付解决方案。其实币安收0.000001BTC都算贵了,我后来发现,OneKey的闪电钱包转出零费用。

我更关心的是安全问题,生成闪电钱包的时候,并没有涉及到助记词之类,我以为是单纯由OneKey的软件端生成——就像其它软件版的闪电钱包,而且由于闪电钱包是侧链钱包,也就是热钱包,本身并不存活于BTC主链上,所以我以为它并不依赖于硬件钱包。

于是,我问了OneKey的官方,客服回答我说,闪电钱包是通过硬件钱包的助记词生成的,因此通过将助记词导入到其它硬件钱包或者软件钱包,可以恢复闪电钱包。

何不自己亲自试一下?我便在OneKey的另一个软件钱包里连上原来的硬件钱包,重新主动生成一个闪电钱包,发现先前软件端的闪电钱包重现了,金额和转账历史完全一样,证实了OneKey客服说的是对的。

由于硬件钱包生成时助记词并没有触网,这意味着由此生成的闪电钱包也是安全的,除非OneKey的闪电节点出问题,但这个风险应该是极小的,何况闪电钱包上一般金额也不多。

许多事情还是得亲自体验才能加深理解。直到今天,我对BTC闪电钱包的使用,才算有了一点点理解。

泼天富贵?以太坊ETH现货ETF内幕猜想

26 May 2024 at 00:00

ETH

美国以太坊ETH现货ETF申请上市最近取得了出乎意料的积极进展,预计接下来几个月,几乎没有悬念会得到最终批准。这背后的内幕原因到底是什么呢?

我有一个猜测,美帝今年1月份批准了BTC的现货ETF,现在又出乎意料地要批准ETH的现货ETF,可能是基于以下两个原因。

  1. 现任总统拜登为了讨好年轻人;
  2. 为了即将到来的美元降息准备一个蓄水池。

关于第一个原因,美国前总统特朗普在自由党全国大会上发表演讲称,将确保加密货币与比特币的未来发生在美国,将支持美国5000万加密货币持有者自托管的权力,将坚决反对央行数字货币,将停止拜登摧毁加密货币的行动,将让加密货币的反对者Elizabeth Warren远离比特币。

美国总人口数量不过3亿多,5000万加密货币持有者如能换成选票,无论如何都不敢让总统候选者们小视,几乎占了六分之一的人口数量。以过去只支持美元的特朗普现在对加密行业如此友好的态度——最近还支持通过加密货币赞助他选举,现任总统拜登再不搞点动静出来,好几千万年轻选民可能就彻底失去了。批准BTC的现货ETF之后,再一次突然加速ETH的ETF批准,让对ETH不太友好的SEC加速审批,只能说明这个问题是一个政治问题。

至于第二个原因,市场都知道美联储今年迟早要启动降息,否则它自身也受不了加息带来的负面作用——高额的资金成本。降息增加资金流动性,相当于市场上增加了许多货币,这些货币要流向哪里?哪一个蓄水池能接住这些突然多出来的钞票呢?美国国债的规模现在是34万亿美元,现货BTC ETF到目前市值也不过是540亿美元,相比国债规模,只占千分之二都不到,是不是起不到什么作用?蓄水池的说法有点牵强?

其实不然,我们需要用增量的思维来理解。使用AI分析可知,让我们假设美联储降息0.25个百分点(25个基点),为市场带来可能的影响:

  • 企业和个人贷款增加:如果降息导致商业贷款增加10%,并且商业银行体系的总贷款余额是1万亿美元,那么新增的贷款金额可能是1000亿美元。
  • 住房市场:降息可能会刺激房地产市场的活动,假设住房贷款增加5%,在一个5000亿美元的市场中,这意味着额外的250亿美元流入市场。

降息带来的增量货币供应和BTC ETF的市值量级相当,再加上即将到来的ETH的ETF市值,它充当蓄水池的功能理论和实际上完全存在。

常言说,种菜不看季节要吃亏。美元利率这个大周期叠加总统选举期,是一个大的机遇。也许这泼天的富贵泡沫就是天降良机,就看什么人用什么工具来接了。

每次泡沫来临的时候,有一部分人选择积极拥抱泡沫,有一部分智者保持理智,认清并且远离泡沫。前者收获了财富,后者收获了智慧。

郑重声明:以上言论不构成投资依据。


如有必要,欢迎添加土木坛子的私人微信(tumuhk),务必备注“美股”,我们一起来探索投资之路,分享有价值的信息。新加坡华侨银行OCBC开户介绍了相关问题。

新加坡OCBC华侨银行线上开户完全指南

19 May 2024 at 00:00

春节前,我亲自去香港开通了我的香港银行账户玩美股理财,有很多朋友私下咨询我相关问题,看来大家对境外投资、境外银行开户的事情很感兴趣。

香港银行开户有一个硬伤:必须本人亲自去一趟香港办理(香港中国银行支持肉身到香港通过网络办理),内地隔离见证办理的话,门槛太高(见下图)

香港离岸开户信息对比

最近研究发现香港之外的证券商像长桥新加坡账户,偶然发现可以实现香港证券无法进行的投资。我这才发现相比香港银行卡,新加坡银行账户也有独特的优势。并且,新加坡华侨银行OCBC完全支持在中国内地远程在线开户,无需亲自线下去银行柜台。即使比起我的英国银行账户,新加坡的银行账户也有其独特优势,它离中国近,对华友好,金融中心,且对中国免签国家,比起其它国家,真有需要去一趟新加坡也相对容易

之前答应整理一个新加坡华侨银行OCBC远程开户教程,下面我介绍一下新加坡OCBC银行线上开户的步骤。

首先,为什么要开OCBC账户?优势明显:

  • 最容易开的离岸账户,接受APP线上开户,并且是实体银行——新加坡第二大银行。
  • 对比香港,资产配置方便、有效分散风险,新加坡是中立国。
  • 有了离岸账户,才能方便境外炒股,买美元货币基金,可投资美股、港股、新加坡股票——能入金盈透证券、嘉信理财、富途牛牛、长桥证券。
  • 用介绍人代码:528S37JB 开户奖励15新币,约人民币80元。

其次,申请华侨银行OCBC账户,首先你需要准备以下资料文件:

  • 有效期超过6个月的个人护照。

  • 有NFC功能的智能手机——需要NFC扫描护照内置芯片。

  • 有效期内的身份证——需要拍摄身份证正面。

  • 本人亲自操作——需要人脸识别认证。

  • +86的大陆手机号——需要接收验证码。

  • 最少准备1050新币(官方要求是1000新元,约5600元)入金验证激活后有15 SGD奖励充值超过1000 SGD激活账户。

下面是具体的通过OCBC APP远程开户步骤详细攻略

1、选择语言:APP安装完成(苹果手机应用市场搜索OCBC Digital,安卓华为应用市场也有,也可在官网 www.ocbc.com 找到),打开后把语言换成中文,点击注册成为新用户。

OCBC开户步骤

2、注册方式:选择电子护照,然后再点击“开始申请”。

OCBC开户步骤

3、输入介绍人代码:528S37JB (通过它可得到15新币,人民币80元左右,不输入介绍人代码也可以开户,但没有任何优惠)。

OCBC开户步骤

4、提供你的联系方式:输入自己的联系方式,可以选择中国大陆+86的手机号的,再输入常用邮箱,进入下一步,收取手机验证码后再进入下一步。

OCBC开户步骤

5、护照编码录入:打开护照的照片页面,用手机扫描一下底部的条形码。

OCBC开户步骤

6、NFC识别护照芯片:把护照合上,对着护照封面扫描(确保手机开启NFC),识别过程最好一手拿着护照一手拿着手机严丝合缝贴在一起慢慢移动,读取的时候(有绿点闪动)就不要动了,绿点点都加载完就说明扫描成功了。

OCBC开户步骤

7、身份证和人脸识别:扫描身份证正反面,根据要求摇头点头眨然后人脸识别验证,眼就行——注意光线要充足。

OCBC开户步骤

8、核对邮寄地址:国家选中国,邮寄地址默认用你身份证地址翻译过来的。实际收件地址不一样就取消勾选”使用我的登记地址”,然后填一个新地址,一定要在地址第一行最后加上自己的手机号,避免跨境信件邮政投递不到位,还可以电话联系。

OCBC开户步骤

9、就业信息:工作类型选受雇人士,资金来源选就业。

OCBC开户步骤

10、税务信息:税务识别号选择“是,我有”,税号输入身份证号。政治国家选择中国,工作任务选“不,未曾担任”。

OCBC开户步骤

11、接收通知消息渠道:勾选“电子邮件”、“短信”,勾选”本人确认懂英文”。提交审核。

OCBC开户步骤

12、开户审核:大多数人都是秒审秒开户,部分人需要等3-7个工作日内审核。

OCBC开户步骤

关于OCBC开户有相关问题或对境外投资有兴趣,如有必要,欢迎添加土木坛子的私人微信(tumuhk),务必备注“美股”,我们一起来探索投资之路,分享有价值的信息。

请注意几个细节

  • 注册者存入不低于1050 SGD(官方要求是1000新元),且新存入的资金必须来自申请者名下的另一个银行账户,才能获得15 SGD。

所有账户首年免管理费,账户超过6个月没余额也不活跃,会自动关闭,也可以找客服申请关户,开户6个月内关户需要收取30新元的关户费。第2年开始的管理费:

  • STS账户:外国人日均存款不超过2万新元收取10新元/月。
  • GSA账户:不需要管理费。
  • 360账户:必须在APP上申请360账号默认分配一张VISA借记卡,借记卡最大的好处是更换手机的时候作为验证,其次是可以本地消费和海外消费(比如支付ChatGPT Plus)、ATM取款等。网页版申请360账号没有借记卡。外国人日均存款不超过3000新元收取2新元/月MSA账户(Monthly Savings Account):日均存款不超过500新元收取2新元/月。

目前比较合适的保号方案是保留GSA账户+360账户或MSA账户。MSA账户需要到网页版OCBC申请开通,APP没有入口。

密码设置、国际购汇汇款方法见下图(可保存图片放大查看)

国际购汇汇款方法

国内如何购买美国和香港现货Bitcoin ETF资产?

17 May 2024 at 00:00

上次提到,内地投资客通过香港券商平台不可购买美国现货Bitcoin ETF一事,后来发现也不能购买香港的现货Bitcoin ETF,其原因是根据香港《联合通函》要求:虚拟资产相关产品的销售必须符合相关司法管辖区的要求,即虚拟资产现货 ETF 禁止向中国内地投资者销售。

香港券商因为要遵守上述规定,所以不得对中国大陆投资客开放。最近我偶尔发现,通过其它地方的券商是可以绕开上面的限制。

具体而言,比如中国大陆人士通过注册长桥新加坡账号,然后就可以轻松购买美国和香港的现货Bitcoin ETF了(当然也可以包括香港的ETH ETF)。原因也不言自明,因为新加坡不像香港受中国大陆的法律连带管辖,也没有类似香港上述的规定,因此可以自由购买加密资产。

我也是偶然间发现这个通道,一朋友注册了券商长桥新加坡账号,然后用他的新加坡银行OCBC(新加坡OCBC华侨银行线上开户完全指南)入金后,实操发现可以自由购买香港和美国现货Bitcoin ETF(香港还有ETH的ETF),实在是感叹小国新加坡真是一个中立且对华人友好的投资宝地,香港的政策对大陆用户依然有很大的限制。

否则,无论是买现货Bitcoin还是现货Bitcoin ETF,对想接触的中国大陆朋友,难以操作实现或者不方便实现。ETF资产因为是传统的证券系统,对大部分人来说,既方便也安全,ETF比起拥有现货来说,最多费一点点管理费,但换来的好处是明显的。

关于长桥新加坡账号可以购买加密ETF资产的新发现,我问过长桥的工作人员,对方拒绝回答我的问题,而是给了我一个电话,总结起来就是四个字:懂的都懂。

但我在这里分享给我的读者们,为你们填平这个信息差,我也不做过多解释,因为:懂的都懂。


以下信息为想开通长桥新加坡或香港账号的朋友而特意准备

承蒙厚爱,长桥官方给土木坛子博客读者配置了一个渠道专属注册优惠,推荐码是:783XQW,专门的注册开户链接:

香港长桥:https://app.longbridgehk.com/ac/oa?account_channel=lb&channel=HB100006&invite-code=783XQW

新加坡长桥:https://activity.lbmkt.ing/pages/longbridge/7415/index.html?app_id=longbridge&org_id=1&account_channel=lb&lang=zh-CN&channel=HB100006&invite-code=783XQW

比如以下渠道专属活动,注册后(注册时填写土木坛子渠道邀请码:783XQW)奖励以下福利:

5月长桥香港首次净入资2万HKD奖励(长桥新加坡账号入资对应等值新币,奖励相应免佣等福利):

  • 福利1:400港币股票现金卡
  • 福利2:5USD期权现金卡
  • 福利3:港股终身免佣
  • 福利4:美股终身免佣
  • 福利5:港股现金打新免费
  • 福利6:5%货币基金 认购资格
  • 福利7:填渠道邀请码:783XQW 额外领取88元京东卡

相关问题如有必要,欢迎添加土木坛子的私人微信(tumuhk),务必备注“美股”,否则不予通过。如有相关问题,也欢迎直接扫描下面微信群二维码,我们一起来探索投资之路,分享有价值的信息:

土木坛子

郑重提醒:投资有风险,你赚不到你认知以外的钱。

香港和中国内地关于资本利得税的相关规定

12 April 2024 at 00:00

money

最近查阅了关于中国内地和香港的个人投资的资本利得税问题,得到的结论有如下一些。

香港个人投资资本利得税

在香港,个人投资的资本利得税制度非常宽松,甚至可以说是零税率。香港税收制度相对简单和低税率是吸引国际投资者的重要因素之一。

香港没有针对个人投资的资本利得征收单独的资本利得税,因此个人投资者在出售资产(如股票、房地产等)获得的资本利得通常不受香港税务局的征税:个人投资者在香港获得的资本利得免税。

在某些特殊情况下,可能会适用资本利得税。例如,个人在交易中作为贸易者频繁买卖证券以谋求利润,可能会被视为属于商业性质,从而被要求缴纳企业税。

在香港,对于出售加密资产所得的资本利得一般也适用类似的税收原则。

但香港征收股息税,从公司获得的股息可能会受到20%的股息税的影响。

此外,香港采用的税收制度是“全球所得原则”,只管收入的来源发生地,这意味着在香港居民收到来自香港以外的收入时,通常不需要缴纳税款。这一原则也适用于资本利得,即使是来自海外的资本利得,只要个人是香港税务居民,通常也不需要缴纳资本利得税。

中国内地个人投资资本利得税

中国大陆(内地)存在资本利得税,但在实际执行中,个人资本利得税的征收并不普遍,且税率相对较低。

税率:内地对个人资本利得征收的税率目前为20%,这适用于股票、债券、基金等金融资产的交易所获得的利得(但似乎暂时不收)。

免税额:内地对于个人资本利得设有一定的免税额度。目前,对于股票交易,个人每年的免税额度为6万元人民币;对于其他金融产品的交易,免税额度为4万元人民币。超过这一免税额度的资本利得将按照20%的税率征收个人所得税。

境外汇款到境内的纳税问题

如果在香港获得投资收益,面临汇往内地的问题,这就涉及到境外向境内汇款纳税的问题。

假如我收到境外朋友汇入中国内地给我的收入,需要纳税吗?

根据中国大陆的税收规定,境外个人向中国大陆境内个人汇款给予的收入通常需要纳税。

但是,如果收到境外朋友赠送给自己的礼物或者生活费用,通常情况下不需要缴纳个人所得税。

礼物:如果收到的款项是境外朋友作为礼物送给您的,通常情况下不被视为收入,因此不需要缴纳个人所得税。中国大陆的个人所得税法规中通常对礼物的收入免税。

生活费:如果收到的款项是境外朋友给的生活费用或者帮助,通常也不需要缴纳个人所得税。这类款项通常不被视为收入,因此不需要纳税。

假如将个人境外银行的资金汇入本人中国境内的银行账户需要纳税吗?

将境外银行的资金汇入中国境内银行账户一般不会触发个人所得税。这类资金往往被视为个人财产的转移,不被视为收入,因此不需要缴纳个人所得税。

我不是税务法专家

以上信息是我查阅到的一些信息,希望能帮到有这方面需求的朋友,资金合法合规是富豪们始终需要注意的事情,依法纳税和合理避税并不矛盾。

必须声明,我不是税务方面的专家,法律条文于我读起来很是晦涩难懂。如果我的读者们有熟悉相关领域的知识,还望不吝赐教,指出我的错误和补充更准确的信息。

省钱秘笈:把域名迁移到Cloudflare

10 April 2024 at 00:00

钱

之前看到Justyy博主把域名迁移到了Cloudflare,于是上周末我也抽空把所管理的域名从Namesilo和Namecheap迁移到了Cloudflare。

最主要的原因是Cloudflare的续费价格便宜得多,COM域名只需要不到10美元一年,而Namesilo再便宜也要14美元左右一年。毕竟Cloudflare号称批发价,不图域名注册和续费上赚钱。

同时,迁移到Cloudflare的话,也方便管理,网站都使用了Cloudflare的CDN全球分发系统,并且免费套餐就够用了。

经济下行,开源节流,各项开支也要节省,能省的还是省一下。每个域名一年节省4美元,数量多起来,年头久了也能节省一笔开支。

像土木坛子网站域名,从2011年3月1日在比利时的Google注册,后来几经迁移到Godaddy再到Namesilo,如今再落新家Cloudflare,续费已经续到了2033年。

说起来Cloudflare也是近年来互联网之光,提供免费好用的CDN服务就不用说了,还提供了很多其它好用的服务,比如免费Warp,看得出来它为互联网的安全高速互联做了极大的贡献。

不知大家还有什么好东西分享和互联网省钱秘笈?分享万岁。

通过香港券商平台可购买美国现货Bitcoin ETF吗?

9 April 2024 at 00:00

长桥券商

前一阵研究Bitcoin现货ETF,于是在长桥券商APP中尝试购买美国的Bitcoin现货ETF。看着一股也不过几十美元,买卖是如此之方便,手续费和管理费也很低,还不用担心托管的风险问题,比持有现货BTC省心多了,心里乐滋滋的。

等我真下单买入的时候,券商弹出页面,说因该产品风险较高,需要认证为专业投资者,什么是专业投资者,很简单,要么在券商那里持有等值800万港元以上资产,或者上传持有等值800万港元以上资产证明。

这么一个门槛,基本非高净值人士不能称之为专业投资者,那么与普通人也就无缘了。与其这样,还不如直接购买现货BTC。

没想到还有更绝的事情。后来和券商的工作人员聊天,他告诉我,哪怕是拥有800万港元资产,根据香港的证券监管要求,中国内地客户也不能购买……

好吧,中国内地这个标签居然是如此之神奇,被保护得无论如何美帝都割不到我们的韭菜,我们得到的关爱是如此之深厚。

从此也可以看出香港的证券市场监管得很严格,严格区分客户来源。只要是中国内地的客户,都是严阵以待。

就像现在香港持牌交易所Hashkey也是不让内地人注册使用,除非是用护照和国外的地址证明及居留许可的华人,才有可能注册认证,可是那样的客户又有什么理由使用你Hashkey呢?

我估计即将批准的香港Bitcoin现货ETF,也会执行相当严厉的政策来对待中国内地投资者:根据《联合通函》要求,虚拟资产相关产品的销售必须符合相关司法管辖区的要求,即虚拟资产现货 ETF 禁止向中国内地投资者销售。

只是不能让中国内地用户参与的新兴资产和交易所有多大发展前途呢?香港的监管者难道没有想到这一点?好消息是,只要还有难度,说明这些新兴资产还有发展空间。

PS 本文发布时,Bitcoin现货报价70232美元。

借道香港玩美股,为伊消得人憔悴

3 April 2024 at 00:00

money

早上测体重,发现最近减掉了一些体重,最大的原因可能是最近在研究通过香港券商投资美股,导致“衣带渐宽终不悔,为伊消得人憔悴”——我可能实在太喜欢研究投资赚钱了,因此也没有时间来更新博文。但深入研究并有所收获,内心很愉悦,这是人到中年难得的乐趣。

为什么聚焦香港的银行和投资服务?

  1. 香港无外汇额度限制;
  2. 香港不征收个人投资的资本利得税,对合法资金相当自由;
  3. 业务上如果有问题,亲自跑一趟香港处理也方便——所以我个人基本上闲置了已有的英国和欧洲的银行系统;
  4. 香港在可见的将来是非常合适大陆用户的金融中转站,通过香港自由对接全世界。

有朋友说身为中国大陆用户,不能玩转美国股市。这个问题的答案既是也不是。

对于中国内地朋友,如今开通香港美股券商账号的必要条件是拥有香港或者其它境外银行账户(卡),这才能表明是境外存量投资内地用户(因此我年前亲自去香港办理了这些银行账号),有了这个必要条件后,只需要3步:

  1. 有了香港银行账号或者境外银行账号后,外汇汇款到境外银行。比如内地中国银行,按“购汇出境和外币服务>结汇购汇>购汇”的路径,填入境外银行账号相应的账号信息接收(中国银行内地账户和香港账户之间来往免手续费,每人每人有5万美元外汇额度);
  2. 之后再开通境外券商账户;
  3. 最后境外券商账户入金(出金),操作美股和美元货币基金(港股和港币货币基金当然也可以)。

所以中国内地朋友要涉足美股,也不是不可以,办法还是有的。

因为我使用的是香港银行系统,选择的券商账户自然使用香港券商。

经博友罗磊的介绍,我这一段时间一直在使用的券商是香港长桥(大体和老虎和富途差不多),用内地手机号和身份证信息注册,用香港银行入金出金,购买美股、美元基金都很顺畅(具体操作细节可参考罗磊的博文)。为了满足少量朋友需要,这里介绍一下我自己正在使用的这家券商。

关于长桥:Longbridge长桥是一间港美股券商,由来自新加坡和香港的资深金融管理者,以及来自阿里巴巴、字节跳动等全球顶尖互联网企业的技术专家组成的创始团队。由新加坡辉立集团旗下辉立资本、知名风投元璟资本(吴泳铭创立 其为阿里集团现任CEO)、领沨资本等机构领投。以及包括陆兆禧(阿里前CEO 荣誉合作人)、王刚(阿里原副总裁)、麻长炜(阿里十八罗汉)、曹毅、唐岩等在内的多位明星投资者注资。

承蒙厚爱,长桥官方给土木坛子博客读者配置了一个渠道专属注册优惠,推荐码是:783XQW,专门的注册开户链接:

https://app.longbridgehk.com/ac/oa?account_channel=lb&channel=HB100006&invite-code=783XQW

比如以下渠道专属活动,注册后(注册时填写土木坛子渠道邀请码:783XQW)奖励以下福利:

5月长桥香港首次净入资2万HKD奖励:

  • 福利1:400港币股票现金卡
  • 福利2:5USD期权现金卡
  • 福利3:港股终身免佣
  • 福利4:美股终身免佣
  • 福利5:港股现金打新免费
  • 福利6:5%货币基金 认购资格
  • 福利7:填渠道邀请码:783XQW 额外领取88元京东卡

有了香港的银行账号和券商账户后,我终于解答了一朋友的问题。他问我,如何利用其赚得的现金躺赚被动收入?我帮其研究的结果是:假如他要实现40万年收入,只需要购买730万左右的美元货币基金,按每年5.5%的年化利率,就相当于实现了年入40万的财务自由。美元基金风险极低,每年管理费用只需0.20%左右,简单易用。

对应内地的人民币货币基金,眼下一般最高也只有2.5%左右的年化收益率,考虑到未来的人民币兑美元还要贬值(目前是7.23),则美元货币基金的实际收益率更高(当然美元降息也会影响美元收益率)。

相关问题如有必要,欢迎添加土木坛子的私人微信(tumuhk),务必备注“美股”,否则不予通过。或者直接扫描下面的微信群,我们一起来探索投资之路:

土木坛子

郑重提醒:投资有风险,你赚不到你认知以外的钱。

重构博客友链页面 & 友链朋友圈开源

By: prin
13 March 2024 at 00:00

先来看看效果:友情链接 - PRIN BLOG

自我感觉还是不错的,友链的博客们有什么更新都可以实时展示在页面上,一目了然。作为博主,不用打开 RSS 阅读器就可以查看新文章;作为访客,也可以快速找到更多自己感兴趣的内容,比起原来全是链接的页面,看起来也让人更有点击欲望了。

从临时起意到开发完成总共两个晚上,最速传说就是我!(误)

缘起

前段时间看到有个博客用了这样的一个东西:

当时就感觉卧槽好高端,很有想法。

这种聚合订阅的形式有个名字,叫做 Planet(社区星球)。Planet 通常用于聚合某个领域的博客,然后展示在一个页面上,方便用户一站式阅读,比如:

这种形式在开源社区里比较常见,不过用在博客的友链上我倒还是第一次看到。


就像我在本站友链页里说的一样,独立博客之间的联系基本上就是靠的链接交换和评论互访。一个博客的访客看到了其他博客的链接,点过去看了,然后从对面的友链中,又导航到新的博客……如此往复,我们就依靠着这种从现在看来显得十分古老的方式,维系着这些信息孤岛之间的纽带。原始又浪漫。

不过这里就会涉及到一个用户点击率的问题。我自己之前在维护友链页面的时候,总感觉只放标题和链接看起来效果不怎么好。就算加上描述、头像这些元素,也总觉得差点意思。因为一个博客最重要的其实还是它的内容,仅靠一个网站标题,可能很难吸引到其他用户去点击。

而「友链朋友圈」的这种形式,就像微信朋友圈一样,作为一个聚合的订阅流,展示了列表中每个博客的最新文章。

比起干巴巴的链接,这显然会更加吸引人。虽然我写博客到现在也已经 9 年了,早就佛系了,主打一个爱看不看。不过对于和我交换了友链的博主们,还是希望他们能够获得更多的曝光和点击(虽然我这破地方也没多少流量就是啦……),也希望我的访客们也可以遇到更多有价值的博客。


然而在准备接入的时候,我发现这玩意儿不就是一个小型的 RSS 阅读器么……其实等于是自己又实现了一套订阅管理、文章爬取、数据保存之类的功能。

于是我就寻思,可能直接复用已有 RSS 阅读器 API 的思路会更好,让专业的软件做专业的事。友链的管理也可以直接复用 RSS 阅读器的订阅管理功能,这样增删改也不需要了,我们就只需要封装一下查询的 API,提供一个精简的展示界面就 OK。

技术栈选择

作为行动力的化身,咱们自然是说干就干,下班回家马上开工!

首先是 RSS 后端的选择。

市面上的 RSS 阅读器有很多,我自己主要用的是 Inoreader。然而我看了下,Inoreader API 只面向 $9.99 一个月的 Pro Plan 开放,而且限制每天 100 个请求……这还玩个屁。Feedly 也是差不多一个尿性,可以全部 PASS 了。我也不知道该说他们什么好,也许做 RSS 真的不挣钱,只能这样扣扣搜搜了吧。

另外一个选择就是各种支持 self-host 的 RSS 阅读器,比如 Tiny Tiny RSSMiniflux。我之前部署过 TTRSS,说实话感觉还是太重了。Miniflux 则是使用 Go 编写的,该有的功能都有,非常轻量级,部署也很方便。就决定是它了!

技术栈方面选择了之前一直比较心水的 Hono,部署在 Cloudflare Workers 上。前端方面没有使用任何框架,连客户端 JS 都没几行,基本上是纯服务端渲染。有时候不得不感叹技术的趋势就是个圈,以前那么流行 SPA,现在又都在搞静态生成了。

cf-workers-usage

页面渲染使用了 Hono 提供的 JSX 方案,可以在服务端用类似 React 的语法返回 HTML,挺好用的。不过 CSS 没有用 Hono 的那一套 CSS-in-JS,因为要允许用户覆盖样式,所以要用语义化的类名。最后选了 Less,还是熟悉的味道。

前端文件的构建使用了 tsup,配置文件就几行,爽。

实现

实现思路很简单,就是做一个 Proxy 层,把:

这两个 Miniflux 的 API 包一下。这里要注意不能暴露实际的 API Endpoint,避免可能的恶意攻击。API 缓存也要在我们这一层做好,防止频繁刷新把服务打爆。

缓存策略上使用了 SWR (Stale-While-Revalidate):

  1. 拿到 API 响应后,放到 KV 中,同时把时间戳放入 metadata;
  2. 后续从 KV 读取缓存时,对比当前时间和 metadata 中的时间戳;
  3. 如果经过的时间没有超过设置的 TTL,说明缓存有效,直接返回前端;
  4. 如果经过的时间超过了 TTL,则标记缓存为 stale 状态,依然返回前端
  5. 此时,后端在后台重新请求 API,并将最新的响应写入 KV 中;
  6. 下一次再从 KV 读取时,拿到的缓存就是最新的了。

这样可以保证最快的响应速度,以及相对及时的更新速度,比较适合这种场景。

最后的交付形式其实就是两个 HTML 页面,通过 <iframe> 的形式嵌入到网页中。另外参考 giscus 提供了一个脚本,可以设置参数并自动完成 iframe 的初始化,用户只需要引入一个 <script> 标签即可,非常方便:

<script  async  data-category-id="28810"  src="https://blog-friend-circle.prin.studio/app.js"></script>

friends-page-demo

当然也可以作为独立页面打开,有做双栏布局适配:

blog-friend-circle.prin.studio/category/2/entries

开源

新版博客友链朋友圈的所有代码都开源在 GitHub 上,欢迎使用:

👉 prinsss/blog-friend-circle

这个方案和 hexo-circle-of-friends 并没有孰优孰劣之分,只是侧重点和实现方式不同。不过我这个的一个好处是,如果你已经在用 Miniflux 了,那么可以直接复用已有的大部分能力,不需要再起一个 Python 服务和数据库去抓取、保存 RSS,相对来说会更轻量、稳定一些。

如果你选择使用 Miniflux 官方提供的 RSS 服务,甚至可以无需服务器,部署一下 CF Workers 就行了,像我这样的懒人最爱。

永远不要让人知道你靠啥赚钱和赚了多少钱

13 March 2024 at 00:00

ZEN

前几天本来想着给大家分享一个空投,虽然作了预警,但还是做得不够。事后复盘,这个所谓的空投,基本上100%是一个骗局。目的就是为了在最后的提现时骗取手续费,有点像尼日尼亚骗局一样。还有一个可能就是获取海量的用户加入它们的电报群,到时再给一些骗局二次精准收割。

这互联网上和现实生活中,充满了大量的虚假信息和骗子,像我这种老网民,都难免未能充分识别出来,何况更多的年轻网民,骗子们真的能大行其道。虽然此次应该没有朋友和我因此事有金钱上的损失,但想起没有充分识别出这种骗局,还是觉得有点自责,自己的智慧还是不够、眼神不够精明,也对不住各位朋友。

在此过程中,要感谢众多读者和我交流,识别这个骗局,尤其是感谢小辉大神,帮我指出其中的破绽,比如它完全不必关注SHIB的官方X,以及它所谓的转账提现信息是如此之少,姜还是老的辣。

我本来的想法是给大家提供一些福利信息,就像以前分享过的空投信息和虚拟信用卡信息,都是成功和真实的。看来以后分享此类信息时,还需要更为谨慎才是。

难道这又印证了那句古话:闷声发大财?翻译成更为直白的话——据说是成年人之间的默契:永远不要让任何人知道,你靠啥赚钱,赚了多少钱。既然许多信息真真假假,不乱分享也是对的,免得自寻烦恼。

无论如何,多多追寻智慧、多多进步总是对的。

为表达我自省和道歉的诚意,发一个真实确定的小福利吧:

目前针对VCard用户的PayPal转账功能推广,官方可支持土木坛子的读者10个免费开通PayPal转账功能的名额。

  1. 获取奖励名额方式:用户开通并进行首次体验(转账到PayPal,转账金额不限);
  2. 完成后,项目方会根据提供的信息进行核对,给用户返还开卡费和首次体验产生的手续费(只返还固定3U部分,而按3%收取部分不进行返还);
  3. 简单概述,用户先开通并体验一次,项目方会返还PayPal开通费和部分手续费(50U+3U)。

活动中奖规则:

  1. 即日起至2024年03月20日(东八时区,20日为最后一天)在本文末(含微信公众号读者留言)成功留言评论(垃圾评论会自动过滤,同一ID重复留言算一次),之后我从留言读者中用AI随机抽取10个幸运读者。
  2. 你需要注册或者已注册VCard并充值开卡激活——我的VCard注册安装推荐链接(邀请码110316): https://webapp.51vcard.com/#InviteRegisterPage?inviteCode=110316
  3. 获奖朋友将收到电子邮件,回复提供VCard钱包的UID即可领奖。放弃奖品或者24小时内不回复的奖品将另行随机抽取其他候选读者,直至分配完毕。

空投每天3次53天可得20000元?骗局!

11 March 2024 at 00:00

Update: 截至今天3月26,都没有收到提现,彻底证实此空投就是之前预测的骗局!请大家不要上当。

Update: 我亲身试验,刚刚(3月11日下午)到达1亿提现门槛,目前完成提现申请,告知是3月25日转账,目前提现操作时没有要求手续费。但网上有人说此种骗局的套路是:提现有门槛让你拉人参加->等到了门槛如果人不够多会告诉你“当前提现人太多,需要排队x个月后到账”。有可能是这种思路……

由此我基本可以判断,这个空投应该是一个假空投,极有可能是骗局,我将在3月25日后根据结果再更新,证实我上面的判断。网络世界假的东西太多,不得不小心为妙。

免责声明:需要强调的是,由于我还没有搞懂它背后的逻辑和动机,到目前也没有达到提现门槛,所以提醒大家一定要注意风险,谨防上当受骗!不要花任何费用去参加(不排除提现的时候骗取手续费)!不要点击不明不白的链接!

空投

一起来薅羊毛!Meme币Shiba Inu coin最近有一个空投活动,方法非常简单(复杂的不免费的空投我不推荐),对国内朋友唯一的门槛可能是:你得会魔法上网。

方法总结为一句话:在电报Telegram上关注任务账号,关注后完成领取2个任务,第一个是关注SHIB的官方X账号;第二个是关注另外的两个Telegram频道。

具体操作方法见下。

  1. 首先点击打开SHIB领任务链接(会自动打开Telegram):https://t.me/OfficialShibaAirdropBot?start=1098851972
  2. 然后点击Start,选择“Join Shiba Inu Airdrop”;
  3. 再按照任务要求,打开第一个链接X的官方账号并关注;
  4. 后面两个都是在Telegram里面打开并关注相应的Telegram频道即可。

以上步骤完成后,就自动获得2000万SHIB奖励了。

以后每隔6小时点击“Hourly Bonus”可领取新奖励50万SHIB,累计到1个亿可以提现(按每天领取3次,需要53天可达到1亿的提现门槛),提现之后卖成任何想要的货币或加密资产。

建议大家抱着玩一玩的心态——不知道会不会有大佬开发自动领取奖励的代码,不要太上头——你不会半夜也醒来领取奖励吧?毕竟拿到手也不过3100美元左右——按现在的SHIB价格0.00003109美元的价格计算,合人民币2.2万元左右,要是接下来SHIB价格再上涨那就更乐观了。

土木坛子在本博之前分享过的HEX, steemit, Stellar, Byteball, HI币空投,收益都还不错,也欢迎大家分享其它纯免费、操作不复杂的空投项目,独乐乐不如众乐乐。

真的不可以在 React 组件内部嵌套定义子组件吗?

By: prin
26 February 2024 at 00:00

最近在 Code Review 时,看到有同事写了这样的代码:

function TodoList() {  const [list, setList] = useState([]);  const TodoItem = useCallback((props) => {    return <li>{props.text}</li>;  }, []);  return <ul>{list.map((item, index) => <TodoItem key={index} text={item} />)}</ul>;}

有经验的 React 开发者肯定一下子就看出问题了:在组件内部嵌套定义组件,会导致子组件每次都重新挂载。因为每次渲染时,创建的函数组件其实都是不同的对象。

但是他又有包了 useCallback 让引用保持一致,好像又没什么问题……?

这波骚操作让我突然有点拿不准了,所以今天咱们一起来验证一下,用 useMemo 或者 useCallback 包裹嵌套定义的子组件,对 React 渲染会有什么影响。以及如果有影响,应该如何用更合适的方法重构。

TL;DR

先说结论:

永远不要在 React 组件内部嵌套定义子组件。

如果你有类似的代码,请使用以下方法替代:

  1. 把子组件移到最外层去,将原有的依赖项作为 props 传入
  2. 把子组件改为「渲染函数」,通过调用函数插入 JSX 节点

为什么?请接着往下看。

组件重新挂载会造成的问题

来看这段代码(可以在 StackBlitz Demo 中运行):

function TodoList() {  const [list, setList] = useState([]);  // 嵌套定义子组件(好孩子不要学哦)  const TodoItem = (props) => {    return <li>{props.text}</li>;  };  const handleAdd = () => setList([...list, `Item ${list.length + 1}`]);  return (    <div>      <button onClick={handleAdd}>Add</button>      {/* 渲染刚才定义的子组件 */}      <ul>        {list.map((item, index) => (          <TodoItem key={index} text={item} />        ))}      </ul>    </div>  );}

可能不少初学者都写出过类似的代码:JavaScript 语言可以嵌套定义函数,React 函数式组件就是函数,那 React 组件不也可以嵌套定义?

还真不是这么回事。我们来实际运行一下这段代码看看:

Tips: 这里使用了 useTilg 这个库来展示组件生命周期。

nested-component

可以看到,每次点击 Add 按钮在 <TodoList/> 列表中添加元素时,之前旧的 <TodoItem/> 组件实例就会被卸载 (unmount)、销毁。React 会创建全新的组件实例,然后再挂载 (mount) 上去。

也就是说,这些组件实例全都变成一次性的了

这还只是一个简单的示例,如果是实际应用场景,一个组件和它的子组件中,可能包含了成百上千个 DOM 节点。如果每次状态更新都会导致这些组件和对应的 DOM 节点被卸载、创建、重新挂载……那应用性能可就是「画面太美我不敢看」了。

更严重的是,组件的卸载还会导致其内部的状态全部丢失

那怎么会这样呢?这要从 React 的渲染机制,以及 Reconciliation 流程说起。

React 渲染机制之 Reconciliation

我们知道 React 的渲染大致可以分为两个阶段

  1. Render 阶段:执行组件的渲染方法,找出更新前后节点树的变化,计算需要执行的改动;
  2. Commit 阶段:已经知道了需要执行哪些改动,于是操作真实 DOM 完成节点的修改。

其中,「找出变化 + 计算改动」这个过程就被叫做 Reconciliation (协调)。React 的协调算法可以在保证效率的同时,最大程度复用已有的 DOM,使得对 DOM 做出的修改尽量小。

render-and-commit

▲ 图片来自:Render and Commit – React

那么问题来了,React 怎么知道一个组件对应的 DOM 需要更新呢?

简单来说,React 在比较两棵 Fiber 树时,会从根节点开始递归遍历所有节点:

  1. 如果节点类型和之前一致
    1. 对于 DOM 元素,保持元素实例不变,仅更新有变化的属性
    2. 对于组件元素,需要重渲染的,使用新属性调用组件渲染方法
  2. 如果节点类型有改变
    1. 卸载该节点及其子节点 ⚠️
    2. 将对应的 DOM 元素标记为待删除
    3. 创建新的节点
    4. 将新的 DOM 元素标记为待插入

上面所说的子组件被卸载再挂载、状态丢失等问题,其实都是因为它们被判断为了「节点类型有改变」

引用相等性与组件重渲染

在 JavaScript 中,比较值时有两种相等性:

  • 值相等性 (Value Equality),即两个值类型一致,内容也一致
  • 引用相等性 (Reference Equality),即两个对象的引用在内存中指向同一块区域

举个例子:

// 两个长得一样的对象const a = { name: 'Anon Tokyo' };const b = { name: 'Anon Tokyo' };// "引用相等性" 比较 - falseconsole.log(a === b);console.log(Object.is(a, b));// "值相等性" 比较 - trueconsole.log(lodash.isEqual(a, b));console.log(a.name === b.name);

JavaScript 函数也是对象,所以这对于函数(React 组件)也成立。

到这里问题就比较明朗了。

function TodoList() {  const [list, setList] = useState([]);  // WARN: 这个语句每次都会创建一个全新的 TodoItem 函数组件!  const TodoItem = (props) => {    return <li>{props.text}</li>;  };  return <ul>{list.map((item, index) => <TodoItem key={index} text={item} />)}</ul>;}

在这段代码中,每次 <TodoList/> 组件重渲染时(即 TodoList 函数被调用时),其内部创建的 TodoItem 都是一个全新的函数组件。

虽然它们长得一样,但它们的「引用相等性」是不成立的。

回到上一节介绍的渲染流程中,React 在比较节点的 type,使用的是 === 严格相等。也就是说像上面那样不同的函数引用,会被视作不同的组件类型。进而导致在触发重渲染时,该组件的节点及其子节点全部被卸载,内部的状态也被全部丢弃。

如果我用 useMemo 包一下呢

到这里我们已经介绍了「在组件内部嵌套定义组件」会造成问题的原理。

这时候可能就有小机灵鬼要问了,既然组件每次都被判断为是不同 type 的原因是对象引用不同,那我用 useMemo / useCallback Hooks,让它每次都返回相同的函数对象不就行了?

能考虑到这一层的都是爱动脑筋的同学,点个赞!让我们再来试验一下:

function TodoList() {  const [list, setList] = useState([]);  // useMemo 允许我们缓存一个值,每次重渲染时拿到的缓存是一样的  // 这里我们返回了一个函数组件,让 useMemo 把这个子组件的函数对象缓存下来  const TodoItem = useMemo(    () => (props) => {      return <li>{props.text}</li>;    },    []  );  // 或者用 useCallback 也可以,都一样  // const TodoItem = useCallback((props) => {  //   return <li>{props.text}</li>;  // }, []);  const handleAdd = () => setList([...list, `Item ${list.length + 1}`]);  return (    <div>      <button onClick={handleAdd}>Add</button>      <ul>        {list.map((item, index) => (          <TodoItem key={index} text={item} />        ))}      </ul>    </div>  );}

nested-usememo

可以看到,包一层 useMemo 之后,子组件确实不会再被 unmount 了。看起来十分正常呢!

让我们再拿 React.memo 来包一下,在 props 相同时跳过不必要的重渲染:

const TodoItem = useMemo(  () =>    memo((props) => {      return <li>{props.text}</li>;    }),  []);

nested-usememo-memo

OHHHHHHHHH!!

如果一个东西看起来像鸭子,叫起来也像鸭子,那么它就是鸭子。

同理,如果我们通过一系列操作可以让「嵌套定义的 React 组件」在渲染时表现得与「在外层定义的组件」一致,那是不是就意味着这种操作其实也是 OK 的呢?

嗯……答案是:没那么简单。

稍微偏个题,你可能会好奇 Hooks 和 memo 为什么也可以在嵌套定义的子组件内正常使用,因为这看起来和我们平时的用法完全不同。

实际上不管是模块顶层定义的函数组件,还是嵌套定义的函数组件,在 React Reconciler 看来都是独立的组件类型,且在渲染时都有着自己的 Fiber 节点来储存状态,而定义该函数的作用域是什么并不重要。想想看:HOC 高阶组件有时候也会返回内联定义的函数组件,其实是一个道理。

useMemo 的缓存失效策略

第一点,useMemouseCallback 的缓存并非完全可靠。

在某些条件下,缓存的值会被 React 丢弃。如果缓存失效,函数组件就会被重新创建,同样会被判断为是不同的组件类型。React 官方肯定不会推荐你把 Hooks 用于这种歪门邪道的用途。

In the future, React may add more features that take advantage of throwing away the cache—for example, if React adds built-in support for virtualized lists in the future, it would make sense to throw away the cache for items that scroll out of the virtualized table viewport. This should be fine if you rely on useMemo solely as a performance optimization.

Ref: useMemo – React

第二点,useMemouseCallback 都有依赖数组。

虽然上面的示例里嵌套组件定义的依赖数组都是空的,但是我们再想想,什么情况下会想要在组件内部定义子组件,而非将其拆成一个单独的组件呢?最主要的原因就是,这个子组件想要直接访问父组件函数作用域中的某些变量。

function TodoList() {  const [list, setList] = useState([]);  const TodoItem = useMemo(    () =>      memo((props) => {        // 注意看,这里子组件直接使用了父级作用域中的 list 变量        return <li>{`${props.text} of ${list.length}`}</li>;      }),    [list.length]  );  const handleAdd = () => setList([...list, `Item ${list.length + 1}`]);  return (    <div>      <button onClick={handleAdd}>Add</button>      <ul>        {list.map((item, index) => (          <TodoItem key={index} text={item} />        ))}      </ul>    </div>  );}

nested-usememo-memo-deps

从实际测试中可以看到,有了依赖项的 useMemo + 嵌套组件,又退化成了最开始的样子,每次都会被当成不同的组件类型,每次都会被 unmount。之前所做的努力全部木大!(顺带一提用 useRef 也是一样的,有依赖就歇菜)

也就是说,只有你的嵌套子组件完全不依赖父组件作用域时,才能保证 useMemo 的缓存一直有效,才能做到完全不影响渲染性能。但既然都已经完全不依赖了,那么又还有什么理由一定要把它定义在父组件内部呢?

重构包含嵌套组件的代码

所以我再重复一遍开头的结论:永远不要在 React 组件内部嵌套定义子组件。

因为这在大部分情况下会造成渲染问题,即使对这种写法做优化也没有意义,因为一不留神就可能掉进坑里,还有可能会误导其他看到你的代码的人。

如果你的代码库中已经有了这样的 💩 代码,可以使用下面的方法重构。

第一种方法,把子组件移到最外层去。

这种方法适用于子组件依赖项不多的情况,如果有之前直接使用的父级作用域中的变量,可以将其改造为 props 传入的方式。

// 组件定义移到模块顶层const TodoItem = memo((props) => {  return <li>{`${props.text} of ${props.listLength}`}</li>;});function TodoList() {  const [list, setList] = useState(['Item 1']);  const handleAdd = () => setList([...list, `Item ${list.length + 1}`]);  return (    <div>      <button onClick={handleAdd}>Add</button>      <ul>        {list.map((item, index) => (          // 改造后:从 props 传入原来的依赖项          <TodoItem key={index} text={item} listLength={list.length} />        ))}      </ul>    </div>  );}

第二种方法,把子组件改为渲染函数 (Render Function)。

JSX 的本质就是 React.createElement(type),React 节点的本质其实就是一个 JavaScript 对象。你在组件 return 语句中直接写 JSX,和定义一个函数返回 JSX 然后再调用这个函数,本质上是一样的。

function TodoList() {  const [list, setList] = useState([]);  // 这不是函数组件,只是一个「返回 JSX 的函数」(函数名首字母非大写)  // 所以每次渲染都重新创建也没问题,也可以直接访问作用域内的变量  const renderTodoItem = (key, text) => {    return <li key={key}>{`${text} of ${list.length}`}</li>;  };  const handleAdd = () => setList([...list, `Item ${list.length + 1}`]);  return (    <div>      <button onClick={handleAdd}>Add</button>      {/* 调用的时候也和调用普通函数一样,而非组件的标签形式 */}      <ul>{list.map((item, index) => renderTodoItem(index, item))}</ul>    </div>  );}

不过需要注意的是,在使用「渲染函数」时,一定要搞清楚和「函数组件」的区别:

  • 渲染函数虽然和组件一样都返回 JSX,但它不是组件;
  • 渲染函数就是普通 JavaScript 函数,没有状态,也没有对应的 Fiber 节点;
  • 渲染函数只是当前组件的一部分,对于 React 渲染来说没有额外开销;
  • 渲染函数内部不能使用 Hooks,只有组件内部才能使用 Hooks;
  • 渲染函数命名一般以 render 开头,首字母小写(否则容易和组件搞混)。

另外,当渲染函数作为 props 传入其他组件时,它也被叫做渲染属性 (Render Props)。这种设计模式在 React 生态中有着大量的应用,可以放心使用。

结语

最后聊一下,如何避免这类问题的发生。

第一,配置 Lint 规则。

防范于未然,合理的 Lint 配置可以减少起码 80% 的代码规范问题。比如本文介绍的坑,其实完全可以通过 react/no-unstable-nested-components + react-rfc/no-component-def-in-render 规则提前规避。

最好再配合代码提交后的 CI 卡点检查,有效避免因开发者环境配置不当或者偷摸跳过检查,导致规则形同虚设的情况。

第二,定期 Code Review。

代码腐化是难以避免的,但我们可以通过流程和规范提早暴露、纠正问题,减缓腐化的速度。Code Review 同时也是一个知识共享、学习和成长的过程,对于 reviewer 和 reviewee 来说都是。

没有人一开始就什么都会,大家都是在不断的学习中成长起来的。

第三,了解一些 React 的原理与内部实现。

因为我自己就是吃这碗饭的,之前写过 React 的 Custom Renderer,也做过渲染性能优化,所以底层原理看的比较多,自然也就知道什么样的代码对性能会有影响。

我一直以来秉持的观点就是,学习框架时也要学习它「引擎盖下」的东西,知其然且知其所以然。如果你希望在这条路上一直走下去,相信这一定会对你有所帮助。


扩展阅读:

使用 TikZ 在 Hexo 博客中愉快地画图

By: prin
16 January 2024 at 04:00

一转眼就到 2024 年了!大家新年快乐!

前段时间在写文章时,需要一些配图,于是就使用了 TikZ 来绘制。TikZ 是一个强大的 宏包,可以使用代码的形式绘制出各种各样精美的矢量图。

如果你的阅读器看不到上面的 SVG 格式图片,可以点这里查看 PNG 格式。example-tikz-graph

上面的图对应的 TikZ 代码可以在这里找到。然而画是画爽了,想把它贴到博客里时却犯了难——目前竟然没有什么好办法可以直接在博客里使用 TikZ!

TL;DR

咱们废话不多说,直接上结果:我写了一个 Hexo 插件,可以直接把 Markdown 源码里的 TikZ 代码渲染成 SVG 矢量图,然后在博客构建时嵌入到页面 HTML 中,用起来就和 MathJax 写数学公式一样简单。

而且最重要的是渲染完全在构建时完成,浏览器上无需运行任何 JavaScript。同时构建机上也无需安装 环境,因为其底层运行的是 WebAssembly。

👉 prinsss/hexo-filter-tikzjax: Server side PGF/TikZ renderer plugin for Hexo.

npm install hexo-filter-tikzjax

注意:插件安装成功后,需要运行 hexo clean 清除已有的缓存。

安装插件后,只需要在博客文章中添加 TikZ 代码块:

---title: '使用 TikZ 在 Hexo 博客中愉快地画图'tikzjax: true---Markdown text here...```​​tikz\begin{document}  \begin{tikzpicture}    % Your TikZ code here...    % The graph below is from https://tikz.dev/library-3d  \end{tikzpicture}\end{document}```

插件就会自动把代码渲染成对应的图片,非常方便:

TikZ 教程

不做过多介绍。贴几个链接,有兴趣的可以学学:

比如这就是我在写文章时画的图,全部用 TikZ 代码生成,画起来改起来都很方便。

原理

在本插件之前,主流的在网页上渲染 TikZ 绘图的方式是使用 TikZJax。TikZJax 有点类似 MathJax,都是通过 JavaScript 去渲染 语法。

<link rel="stylesheet" type="text/css" href="https://tikzjax.com/v1/fonts.css"><script src="https://tikzjax.com/v1/tikzjax.js"></script><script type="text/tikz">  \begin{tikzpicture}    \draw (0,0) circle (1in);  \end{tikzpicture}</script>

然而这样做的问题是,太重了。在网页上动态加载 TikZJax,需要加载 955KB 的 JavaScript + 454KB 的 WebAssembly + 1.1MB 的内存数据,如果再另外安装一些宏包,最终打包产物大小甚至可以达到 5MB+。

对于一些有加载性能要求的网站,这显然是难以接受的。

那怎么办呢?答案就是 SSR / SSG,把渲染过程搬到服务端/构建时去做。这很适合博客这样的场景,尤其是静态博客,只需要构建时渲染一下,把生成的图片塞到 HTML 里就完事了,完全不需要客户端 JavaScript 参与,加载速度大幅提升。

因为 TikZJax 底层跑的是 WebAssembly,而 Node.js 也支持运行 WebAssembly,所以很自然地我就想,能不能把它的渲染流程直接搬到 Node.js 里面去做?

说干就干。于是就有了 node-tikzjax,一个 TikZJax 的移植版,可以在纯 Node.js 环境下运行,无需安装第三方依赖或者 环境。轻量化的特性很适合拿来做服务端渲染,也支持在 Cloudflare Worker 等 Runtime 上运行,非常好用。

hexo-filter-tikzjax 则是 node-tikzjax 的一个上层封装,主要就是在渲染 Hexo 博客文章时提取 Markdown 源码中的 TikZ 代码,交给 node-tikzjax 执行并渲染出 SVG 图片,然后将其内联插入到最终的 HTML 文件中。

如果是其他博客框架,也可以用类似的原理实现 TikZ 静态渲染的接入。

局限性

因为 node-tikzjax 并不是完整的 环境,所以不是所有宏包都可以使用。目前支持在 \usepackage{} 中直接使用的宏包有:

  • chemfig
  • tikz-cd
  • circuitikz
  • pgfplots
  • array
  • amsmath
  • amstext
  • amsfonts
  • amssymb
  • tikz-3dplot

如果希望添加其他宏包,可以参考 extractTexFilesToMemory 这里的代码添加。

如果在使用插件的过程中 TikZ 代码编译失败了,可以通过 hexo s --debug 或者 hexo g --debug 开启调试模式,查看 引擎的输出排查问题:

This is e-TeX, Version 3.14159265-2.6 (preloaded format=latex 2022.5.1)**entering extended mode(input.texLaTeX2e <2020-02-02> patch level 2("tikz-cd.sty" (tikzlibrarycd.code.tex (tikzlibrarymatrix.code.tex)(tikzlibraryquotes.code.tex) (pgflibraryarrows.meta.code.tex)))No file input.aux.ABD: EveryShipout initializing macros [1] [2] (input.aux) )Output written on input.dvi (2 pages, 25300 bytes).Transcript written on input.log.

或者也可以在这个 Live Demo 中输入你的 TikZ 代码,提交后可在控制台查看报错。

致谢

首先要感谢 @kisonecat 开发的 web2js 项目,这是一个 Pascal 到 WebAssembly 的编译器,使我们可以在 JavaScript 中运行 引擎,也是下面所有项目的基石。

这里有作者关于构建基于 Web 的 引擎的一篇文章,可以拜读一下:Both TEX and DVI viewers inside the web browser

感谢 @drgrice1 对 TikZJax 和 dvi2html 的修改,TA 的 fork 中包含了很多有用的新功能,并且修复了一些原始代码中的问题。

感谢 @artisticat1 对 TikZJax 的修改,这是基于上述 @drgrice1 的 fork 的又一个 fork,也添加了一些有用的功能。本插件依赖的 node-tikzjax,其底层使用的 WebAssembly 二进制和其他文件就是从这个仓库中获取的。

感谢 @artisticat1 开发的 obsidian-tikzjax 插件,这是本项目的灵感来源。本项目和该插件底层共享同一套 引擎,使用语法也很类似,基本可以在 Obsidian 和 Hexo 之间无缝切换(实际上也是我开发这个的原因 😹)。

如有任何问题,请在 GitHub 上提交 issue。祝使用愉快!

详解 PixiJS Filter 中的参数与坐标系

By: prin
3 November 2023 at 21:35

除草啦除草啦,再不更新博客就要变成热带雨林啦!🌿

最近在给一个 PixiJS 程序编写 WebGL Shader,被各种参数和坐标系搞得晕头转向。痛定思痛,整理了一下 PixiJS Filter 系统中的各种概念,以供后续参阅。

在 WebGL 中,我们可以通过编写顶点着色器 (Vertex Shader) 和片元着色器 (Fragment Shader) 来实现各种各样的渲染效果。而在 PixiJS 中,渲染引擎为我们屏蔽了绝大多数的底层实现,通常情况下用户是不需要自己调用 WebGL API 的。如果有编写自定义着色器代码的需求,一般是使用 Filter 来实现。

PixiJS Filter 是什么

PIXI.Filter 其实就是一个 WebGL 着色程序,即一组顶点着色器 + 片元着色器的封装。和 Photoshop 中的滤镜功能类似,它接受一个纹理 (Texture) 输入,然后将处理后的内容输出到 Framebuffer 中。使用滤镜,可以实现模糊、扭曲、水波、烟雾等高级特效

用户只需要定义着色器的 GLSL 代码,传入对应的参数,剩下的工作就全部交给 PixiJS 完成。如果你对这些概念不太熟悉,可以看看:WebGL 着色器和 GLSL

默认的 Filter 着色器代码

在 PixiJS 中,Filter 自带了一组默认的顶点着色器和片元着色器代码。用户在定义 Filter 时,如果省略了其中一个,就会使用默认的着色器代码运行。

new Filter(undefined, fragShader, myUniforms); // default vertex shadernew Filter(vertShader, undefined, myUniforms); // default fragment shadernew Filter(undefined, undefined, myUniforms);  // both default

这是 Filter 默认的顶点着色器代码

attribute vec2 aVertexPosition;uniform mat3 projectionMatrix;varying vec2 vTextureCoord;uniform vec4 inputSize;uniform vec4 outputFrame;vec4 filterVertexPosition(void){    vec2 position = aVertexPosition * max(outputFrame.zw, vec2(0.)) + outputFrame.xy;    return vec4((projectionMatrix * vec3(position, 1.0)).xy, 0.0, 1.0);}vec2 filterTextureCoord(void){    return aVertexPosition * (outputFrame.zw * inputSize.zw);}void main(void){    gl_Position = filterVertexPosition();    vTextureCoord = filterTextureCoord();}

这是默认的片元着色器代码

varying vec2 vTextureCoord;uniform sampler2D uSampler;void main(void){    gl_FragColor = texture2D(uSampler, vTextureCoord);}

看着一脸懵逼不要紧,下面我们逐一解释。

WebGL 知识回顾

先来复习一下 WebGL 基础知识

  • WebGL - 基于 OpenGL ES 的图形渲染 API,可根据代码绘制点、线和三角形
  • 着色器 (Shader) - 运行在 GPU 上的代码,一种将输入转换为输出的程序
  • GLSL - 专门用于编写着色器的语言,语法类似 C 语言
  • 顶点 (Vertex) - 一个 2D/3D 坐标的数据集合,表示空间中的一个点
  • 顶点着色器 (Vertex Shader) - 处理顶点数据,生成裁剪空间坐标值
  • 片元 (Fragment) - 也叫片段,包含了渲染一个像素所需的所有数据
  • 片元着色器 (Fragment Shader) - 计算当前光栅化的像素的颜色值
  • 参数 - 着色器中获取数据的方法,主要有:attribute、uniform、texture、varying

pipeline

▲ OpenGL 图形渲染管线的流程。插图来自 LearnOpenGL

不太熟悉的同学可能会以为 WebGL 是 3D 渲染 API,但其实 WebGL 本身只是一个光栅化引擎,并没有提供什么 3D 功能。如果想要实现 3D 渲染,你需要将模型中的三维坐标点转换为裁剪空间的二维坐标,并提供对应的颜色。WebGL 负责将这些图元绘制到屏幕上,仅此而已。

试想:任何 3D 模型,不管怎样旋转、缩放、平移,最终展示到你的屏幕上,都是 2D 的。同样,不管模型上有什么贴图、光照、阴影、反射,最终改变的其实都是这个像素的颜色值。

再来复习一下 WebGL 中的坐标系统:

  • 局部坐标 (Local Coordinate)
    • 或称模型坐标,对应局部空间
    • 一个物体中的点相对于该物体原点的局部坐标
  • 世界坐标 (World Coordinate)
    • 对应世界空间
    • 局部坐标相对于世界原点的坐标,把物体放到整个世界中去看
  • 观察坐标 (View Coordinate)
    • 对应观察空间
    • 从摄像机/人眼的角度去观察世界,所看到的物体相对于观察者的坐标
    • 同一个世界坐标,从不同的距离、角度、视野去观察,得到的观察坐标也不同
  • 裁剪坐标 (Clip Coordinate)
    • 对应裁剪空间
    • 将观察空间内超出一定范围的坐标点都裁剪掉,只保留一定范围内的坐标
    • 任何超过这个范围的点都不会显示在你的屏幕上
    • 从观察坐标转换为裁剪坐标的过程,称作投影变换 (Projection)
  • 标准化设备坐标 (Normalized Device Coordinate, NDC)
    • 将裁剪空间的坐标值范围映射到 [-1, 1] 范围之间,即为 NDC
    • 坐标 (0, 0) 位于裁剪空间的正中间,左下角为 (-1, -1),右上角为 (1, 1)
  • 屏幕坐标 (Screen Coordinate)
    • 对应屏幕空间
    • 将标准化设备坐标映射到屏幕坐标的过程,称做视口变换
  • 纹理坐标 (Texture Coordinates)
    • 即纹理图像上的坐标
    • 纹理坐标与像素坐标不同,无论纹理是什么尺寸,纹理坐标范围始终是 [0, 1]
    • 纹理图像的左下角坐标为 (0, 0),右上角坐标为 (1, 1)

coordinate-systems

▲ 各种坐标与变换矩阵的关系。插图来自 LearnOpenGL

Filter 中的几种坐标系

下面介绍 PixiJS Filter 中的坐标系,以及它们和 WebGL 坐标系之间的关系。

Input Coordinate

输入坐标,用于表示传入 FilterSystem 的纹理上的坐标。

Normalized Input Coordinate 是标准化之后的输入坐标,即纹理坐标,范围是 [0, 1]

Screen Coordinate

相对于 canvas 视口的屏幕坐标,单位是 CSS 像素。

CSS 像素乘以分辨率 resolution 即为屏幕物理像素。

Filter Coordinate

滤镜坐标,即被 Filter 所覆盖的范围内的局部坐标,单位是 CSS 像素。

Normalized Filter Coordinate 是标准化之后的滤镜坐标,滤镜覆盖范围的左上角坐标是 (0, 0),右下角坐标是 (1, 1)

Sprite Texture Coordinate

额外的图片纹理坐标。可以用于采样其他输入中的纹理。

Demo: DisplacementFilter

Sprite Atlas Coordinate

额外的精灵图集纹理坐标。

Filter 的输入参数

讲完坐标的种类,下面介绍 Filter 着色器代码中传入的各个参数(attributes、uniform、varying)分别代表什么,对应的是什么坐标系,以及它们的取值分别是多少。

⚠️ 以下参数适用于 PixiJS v7 版本,不排除后续版本中有变动的可能。

aVertexPosition

  • 类型:vec2 二维向量
  • 含义:Filter 所覆盖的范围内的标准化坐标
  • 坐标系:Normalized Filter Coordinate
  • 取值范围:0.0 ~ 1.0

假设有一个长宽为 300x300 的矩形 A,其原点左上角位于 (100, 30) 世界坐标处。则有:

  • aVertexPosition (0.0, 0.0) 对应矩形左上角
  • aVertexPosition (1.0, 1.0) 对应矩形右下角

projectionMatrix

  • 类型:mat3 3x3 矩阵
  • 含义:投影矩阵,用于将观察空间坐标变换到裁剪空间坐标

具体是怎么变换的,请阅读本文底部的「投影矩阵」章节。

inputSize

  • 类型:vec4 四维向量
  • 含义:输入 filter 的临时 framebuffer 大小
  • 坐标系:Input Coordinate
  • 取值范围:长宽 > 0,单位 CSS 像素

假设有一个大小为 512x512 的 framebuffer,则 inputSize 的值为:

vec4(512, 512, 0.0020, 0.0020)

其中,前两个分量 x, y 为像素大小,后两个分量 z, w 是像素大小的倒数。倒数可用于将除法转换为乘法,因为乘以一个数的倒数等于除以这个数。

以下两个表达式是等价的:

aVertexPosition * (outputFrame.zw * inputSize.zw);aVertexPosition * (outputFrame.zw / inputSize.xy);

但是在计算机中,乘法比除法的计算速度更快。所以在 GLSL 着色器这类需要高速运行的代码中,通常会尽量避免直接使用除法,而使用倒数乘法替代。

outputFrame

  • 类型:vec4 四维向量
  • 含义:Filter 所覆盖的区域在屏幕坐标中的位置与大小
  • 坐标系:Screen Coordinate
  • 取值范围:位置不限,长宽 > 0,单位 CSS 像素

还是以矩形 A 为例,其 outputFrame 取值为:

vec4(100, 30, 300, 300)

其中,前两个分量 x, y 为输出区域的位置,后两个分量 z, w 为输出区域的长宽。

vTextureCoord

  • 类型:vec4 四维向量
  • 含义:用于采样输入 filter 的临时 framebuffer 的纹理坐标
  • 坐标系:Normalized Input Coordinate
  • 取值范围:0.0 ~ 1.0

uSampler

  • 类型:sampler2D 纹理
  • 含义:输入 filter 的纹理图像,可配合 vTextureCoord 纹理坐标进行采样

inputPixel

  • 类型:vec4 四维向量
  • 含义:输入 filter 的临时 framebuffer 物理像素大小
  • 坐标系:Input Coordinate
  • 取值范围:长宽 > 0,单位物理像素

和 inputSize 类似,但是单位不是 CSS 像素,而是物理像素。以下两个表达式等价:

inputPixel.xyinputSize.xy * resolution

同样地,inputPixel.zwinputPixel.xy 的倒数,用于转换除法为乘法。

inputClamp

  • 类型:vec4 四维向量
  • 含义:用于将纹理坐标钳制 (clamp) 在 framebuffer 的有效范围内
  • 坐标系:Normalized Input Coordinate
  • 取值范围:0.0 ~ 1.0

有效范围指的是临时 framebuffer 中实际包含纹理图像的部分,其余部分可能是透明的(具体原因可阅读文章下方的注意事项)。使用示例:

vec4 color = texture2D(uSampler, clamp(modifiedTextureCoord, inputClamp.xy, inputClamp.zw));

其中,inputClamp.xy 表示有效范围的左上角,inputClamp.zw 表示有效范围的右下角。

resolution

  • 类型:float
  • 含义:分辨率,即 CSS 像素与物理像素的比率,类似 devicePixelRatio

filterArea (legacy)

  • 类型:vec4 四维向量
  • 含义:Filter 所覆盖的区域在屏幕坐标中的位置与大小

注意,filterArea 已经被标记为 legacy,你应该考虑使用其他参数替代。

// 以下语句等价于直接使用 filterArea uniformvec4 filterArea = vec4(inputSize.xy, outputFrame.xy)

filterClamp (legacy)

  • 类型:vec4 四维向量
  • 含义:兼容旧版本的 legacy uniform,与 inputClamp 等价

坐标系之间的转换

Filter 中的各种坐标系直接可以通过公式转换。

参考:v5 Creating filters · pixijs/pixijs Wiki

// Input Coord// 单位:标准化坐标vTextureCoord// Input Coord -> Filter Coord// 单位:标准化坐标 -> CSS 像素vTextureCoord * inputSize.xy// Filter Coord -> Screen Coord// 单位:CSS 像素vTextureCoord * inputSize.xy + outputFrame.xy// Filter Coord -> Normalized Filter Coord// 单位:CSS 像素 -> 标准化坐标vTextureCoord * inputSize.xy / outputFrame.zw// Input Coord -> Physical Filter Coord// 单位:标准化坐标 -> 物理像素vTextureCoord * inputPixel.xy// Filter Coord -> Physical Filter Coord// 单位:CSS 像素 -> 物理像素vTextureCoord * inputSize.xy * resolution // 与上一条语句等价

注意事项

需要注意的是,在应用 Filter 之前,PixiJS 的 FilterSystem 会先把目标的 DisplayObject 渲染到一个临时的 Framebuffer 中。

这个 framebuffer 的大小不一定等于 DisplayObject 的大小,它是一个二次幂纹理 (Power-of-two Texture)。假如你有一个 300x300 的图片要应用滤镜,那么 PixiJS 会将其渲染到一个 512x512 尺寸的 framebuffer 上,然后将这个 framebuffer 作为输入传递给着色器代码。

根据这个 DisplayObject 上的 filters 属性定义,PixiJS 会依次执行数组中的 filter,前一个的输出作为后一个的输入,最后一个输出的将渲染到最终的 render target 中。

不过这个创建临时 framebuffer 的行为可能会在自定义着色器代码中导致一些问题,比如纹理坐标的偏移,有时间后续我会另外发文章讨论。

spector-js

▲ 通过 Spector.js 抓取到的 PixiJS 渲染过程

回顾默认着色器代码

好了,梳理完各种坐标系和参数后,我们再来回头看看上面的默认着色器代码:

// 标准化的「滤镜坐标」,范围是 filter 所覆盖的矩形区域attribute vec2 aVertexPosition;// 投影矩阵uniform mat3 projectionMatrix;// 纹理坐标varying vec2 vTextureCoord;// 输入 filter 的临时 framebuffer 大小uniform vec4 inputSize;// filter 所覆盖的区域在屏幕坐标中的位置与大小uniform vec4 outputFrame;vec4 filterVertexPosition(void){    // position 算出来的是 filter 所覆盖的区域的屏幕坐标    vec2 position = aVertexPosition * max(outputFrame.zw, vec2(0.)) + outputFrame.xy;    // 通过投影矩阵,将屏幕坐标转换为裁剪空间 NDC 坐标    return vec4((projectionMatrix * vec3(position, 1.0)).xy, 0.0, 1.0);}vec2 filterTextureCoord(void){    // 等价于 aVertexPosition * (outputFrame.zw / inputSize.xy)    // 也就是将「滤镜坐标」从 outputFrame 的范围缩放到 inputSize 的范围    // 计算出来的就是 inputSize 范围内的「纹理坐标」    return aVertexPosition * (outputFrame.zw * inputSize.zw);}void main(void){    // 裁剪空间 NDC 坐标传递给 WebGL    gl_Position = filterVertexPosition();    // 纹理坐标传递给片元着色器    vTextureCoord = filterTextureCoord();}
// 纹理坐标varying vec2 vTextureCoord;// 输入 filter 的临时 framebuffer 纹理uniform sampler2D uSampler;void main(void){    // 使用纹理坐标在传入的纹理上采样得到颜色值,传递给 WebGL    gl_FragColor = texture2D(uSampler, vTextureCoord);}

怎么样,是不是感觉清晰了很多呢?

Bonus: 投影矩阵

如果你很好奇上面的投影矩阵是怎么做到乘一下就能把屏幕坐标转换为裁剪空间坐标的,那么这一小节就可以解答你的疑惑。

🤫 偷偷告诉你,CSS 中的 transform: matrix() 也是用了同样的矩阵变换原理。

投影矩阵的默认计算方式如下,代码来自 ProjectionSystem#calculateProjection

// 矩阵表示:// | a | c | tx|// | b | d | ty|// | 0 | 0 | 1 |// 数组表示:// [a, b, 0, c, d, 0, tx, ty, 1]//// 主要参数:// sourceFrame - Filter 所覆盖的区域的世界坐标,长、宽、X、Y,像素单位// root - 控制 Y 轴反转,当渲染到 framebuffer 时投影为 y-flippedcalculateProjection(){    const pm = this.projectionMatrix;    const sign = !root ? 1 : -1;    pm.identity();    pm.a = (1 / sourceFrame.width * 2);    pm.d = sign * (1 / sourceFrame.height * 2);    pm.tx = -1 - (sourceFrame.x * pm.a);    pm.ty = -sign - (sourceFrame.y * pm.d);}

这个投影矩阵主要做了两件事:

  1. 缩放变换,从像素坐标转换到 0.x ~ 2.x 范围
  2. 平移变换,将坐标转换为 -1.0 ~ 1.0 范围内的标准坐标

对于一个长宽为 300x300,原点左上角位于 (100, 30) 世界坐标处的矩形,可得:

  • sourceFrame.width = 300
  • sourceFrame.height = 300
  • sourceFrame.x = 100
  • sourceFrame.y = 30
  • sign = 1 (此处假设为渲染至 framebuffer)

计算出投影矩阵为:

使用矩阵乘法对世界坐标进行变换:

得到如下坐标:

  • 局部坐标:(0, 0) ~ (300, 300)
  • 世界坐标:(100, 30) ~ (400, 330)
  • 缩放变换:(0.67, 0.20) ~ (2.68, 2.21)
  • 平移变换:(-1.0, -1.0) ~ (1.0, 1.0)

即可将世界坐标转换为裁剪空间的标准化设备坐标。数学,很神奇吧!👊

更多关于矩阵变换的资料可参考:


顺便测试一下我的新插件 hexo-filter-tikzjax,别在意~

逆向拼多多上的「关灯神器」,实现蓝牙遥控开关灯

By: prin
1 May 2022 at 05:01

依稀记得以前在某个友链博主那边看到过一篇文章,讲的是因为他们寝室所有人都懒得下床关灯,所以就用树莓派和舵机做了个远程遥控关灯的小玩意儿,当时我就感叹,果然懒才是第一生产力。

自从今年初开始出来租房住,突然就感觉睡前关灯变得好麻烦好麻烦。我的房间里是有好几盏灯的,床头的开关只能控制其中的两盏,剩下的开关在另一个地方,另外还有一个总开关位于进门的门厅处。于是我就陷入了两难之境:

  • 不用总开关:每天睡前把灯一一关掉,第二天回家又得一一开回来;
  • 直接用总开关:开关离床太远,关完灯要摸黑上床,早上起来又得先过去开灯。

不爽,太不爽了!现在都讲究智能家居,我这他喵的是智障家居啊……

作为租房一族,咱们也没法对灯啊开关啥的做电气改造(不然直接换个智能开关就完事儿了),只能使用一些「非侵入式」的方案。首先想到的就是上面提到的开发板 + 舵机,搜了一下似乎已经烂大街了,有不少成熟的方案(ESP8266 居多)。

不过我还是低估了我的懒癌,连动手都不想动了,于是直接去万能的某宝搜索「关灯神器」:

light-switch-products

(为什么不是某宝?别问,问就是消费降级)

哎呀,没想到还真有现成的,竟然还能红外 + 手机遥控,不错哦!

入手「关灯神器」

所谓关灯神器,其实也是一个能接收红外和蓝牙信号的主板,加上一个舵机来控制开关。

product-unboxing

我买的这款是 🐻 卡通款,还带了个小夜灯功能,聊胜于无吧。内置锂电池供电,可以通过 micro USB 接口充电。开模挺精准,普通 86 型墙壁开关完美适配,通过无痕胶和滑槽安装,可以卸下来充电,总体还是挺满意的。

然而,这玩意最操蛋的其实是软件部分……除了附带的红外遥控器,如果想要用手机遥控它开关灯,竟然只能用微信小程序!

微信小程序……小程序……程序……序……

讲道理,我第一次知道微信小程序是还有提供蓝牙能力的,而且还真有人用,我和小伙伴们表示都孤陋寡闻,惊了个呆。

但是这我 TMD 就很不爽了,关个灯我还要打开微信,还得用你的小程序?

rnm

作为一个合格的折腾星人,自然不能如此任人宰割。不就是一个蓝牙设备嘛,小程序能遥控,我难道就不能遥控了?

逆向「关灯神器」小程序

这里主要用到的是 wxappUnpacker 这个工具对小程序解包、反混淆。以 Android 手机为例,小程序的包文件位于:

/data/data/com.tencent.mm/MicroMsg/{hash}/appbrand/pkg/xxxxx.wxapkg

这个目录一般需要 root 权限才能访问,但不巧的是哥已经不折腾 Magisk/Xposed 好多年,手上已经没有 root 过的机器了……不过天无绝人之路,我想起来 MIUI 有自带一个应用数据备份功能,可以备份 App 的 /data 目录。

这玩意儿备份出来的东西其实就是标准的 Android 备份格式 (.ab) 前面加了个自己的文件头,去掉头就可以吃了(划掉),用 Hex Editor 删掉文件头部 414E44 以前的部分,就可以直接当做 .ab 文件处理了。

miui-backup-hex-editor

(谢谢你,雷军!金凡!)

我这里用的是 android-backup-extractor,完整流程如下:

# MIUI 的备份目录adb pull /sdcard/MIUI/backup/AllBackup/20220501_010000/ ./# 去掉 .bak 文件的头部后另存为 .ab 文件java -jar ./abe.jar unpack '微信(com.tencent.mm).ab' mm.tar# 小程序位于 apps/com.tencent.mm/r/MicroMsg/{hash}/appbrand/pkg/*.wxapkgtar xvf mm.tar

目录下可能会有很多小程序的 .wxapkg 包,这里就只能按照时间一个一个试过去了……拿到正确的小程序包以后,使用 wxappUnpacker 解包:

./bingo.sh xxx.wxapkg

解包出来呢,大概就是这样的:

wxapkg-extracted

接下来就是在源码里找控制逻辑和通信值了,看看有没有加密什么的。不得不说,小程序这种前端技术做的东西,确实和裸奔没什么区别,真的能叫做逆向吗……标题党实锤了(作为一个前端仔,看到这些东西就像回家了一样)

随便看了一圈,发现这家制造商的业务线是真的广,光看里面内置的设备类型就有:风扇、茶吧机、干衣机、夜灯、颈椎按摩仪、腰部按摩器、足部按摩器、足浴器、水暖毯、灭蚊器、加湿器、电暖器、按摩椅,感觉像是专门给人生产贴牌智能硬件的,然后遥控模块和小程序用的都是同一套,十分强大。

下面贴几块处理过的关键代码:

// 遥控按钮的入口<i-btn  hover  bindtap="remoteIR"  icon="icon-power"  id="0"  label="大灯"  type="round-big"></i-btn>// 按钮事件处理function remoteIR(e) {  var id = e.currentTarget.id;  // cmd = "01" + "807F" + "12"  // 每种产品都有不同配置,前两个都是固定的,最后的 "12" 代表开关大灯,"08" 为氛围灯  // 还有 "01" 定时十分钟,"03" 定时三十分钟,以及氛围灯亮度等等  var cmd = config.irType + config.irAddr + config.irCMD[id].value;  this.sendCMD("3201", cmd);  this.vibrateLong();}function sendCMD(e, B) {  // format2Byte 函数的作用其实就是补零到 4 位,比如 6 -> 0006  // s = "fe010006320101807F12";  var s = "fe01" + format2Byte(((e.length + B.length) / 2).toString(16)) + e + B;  sendData(s);}

下面的 sendData 也就是实际调用微信小程序 SDK 蓝牙能力的地方:

function sendData(n) {  // ArrayBuffer(10) = FE 01 00 06   32 01 01 80   7F 12  var t = new Uint8Array(    n.match(/[\da-f]{2}/gi).map(function (n) {      return parseInt(n, 16);    })  ).buffer;  wx.writeBLECharacteristicValue({    // 蓝牙设备 ID    deviceId: this.globalData.deviceInfo.deviceId,    // 对应的服务 UUID    serviceId: this.globalData.deviceInfo.serviceId,    // 可写入的特征值 UUID    characteristicId: this.globalData.deviceInfo.writeCharacteristicsId,    // 写入值    value: t,    success: function (n) {},    fail: function (n) {},  });}

简单来说,就是通过 BLE (Bluetooth Low Energy, 蓝牙低功耗) 协议连接开关设备,通过读写对应 Characteristic 的值与其通信,实现设备的控制(如开关灯)。

手动连接设备发送开关灯指令

好了,所有需要的数值现在都已经到手了,下面就尝试跳过微信小程序,手动连接设备发送指令,看看能不能正常操作吧。

这里我用到的是 BLE-调试工具 这个 Android 应用,打开后扫描蓝牙设备,找到并连接「关灯神器」。如果不知道具体是哪个设备,就选看起来比较可疑的。

然后在设备的 Service 中,找到带有 WRITE 属性的特征值 (Characteristic),就是我们用来通信的特征值了。点旁边的写入按钮,把上面逆向出的值填进去……

android-ble-test

见证奇迹的时刻,灯关上了!再次写入同样的值,灯又打开了!

欧耶✌️

还有其他的指令值也可以试一试,比如最后两位改成 08 就是开关氛围灯,等等。

写一个 Android App

想要让这个开关更“智能”,单靠手动操作手机遥控肯定是不够看的。因为手头没有开发板(听说现在树莓派都被炒上天了,不懂),所以还是让闲置的手机发挥余热吧。

好在之前学的那点 Android 开发还没有全忘光,基于 Android-BLE 这个库(其实上面我们用来测试的 App 就是这个库的 demo)和小程序里扒出来的控制逻辑糊了一个遥控 App 出来(代码放在 GitHub):

ble-light-switch

可以看到界面非常简约,不过比什么微信小程序可好用多了。幸福感 UP!

等以后有时间的话,再捣鼓捣鼓接入一下 Home Assistant,加几个自动化,不用动手直接喊 Siri 关灯,岂不美哉?(dreaming)

demo

参考链接

在 M1 Mac 上运行 macOS 虚拟机

By: prin
26 November 2021 at 18:30

Apple M1 芯片问世一年有余,时至今日,在 M1 Mac 上运行 Windows、Linux 虚拟机的方法都已经比较成熟了。然而 macOS 本身的虚拟化却并非如此:直到 Monterey 发布,于 M1 Mac 上运行 macOS 虚拟机才成为可能。

最近有几个小实验需要在 macOS 虚拟机上跑,本来以为去 Parallels Desktop 上开一个就完事了,搜了一下才发现,其实事情没那么简单……实际配置过程中也是踩了几个坑,所以顺带记录一下。

前提

目前想要在 M1 Mac 上运行 macOS 虚拟机,有以下要求:

  • Host OS 和 Guest OS 都必须是 Monterey
  • 安装镜像必须是 IPSW 格式

为什么 Big Sur 不行?因为在 Virtualization framework 中运行 macOS 虚拟机是 Monterey 才加入的功能

那以前怎么就能虚拟呢?因为 ARM 架构的 M1 Mac 在引导上用的其实是 iOS 那一套,不是传统的 UEFI,所以苹果官方没提供 bootloader 的话自然没戏。黑苹果也是一样的道理,只能说且用且珍惜吧。

至于 .ipsw 文件,这玩意其实就是 iOS 固件的格式……真就大号 iPad 呗!

IPSW 镜像文件可以在这里下载:Apple Silicon M1 Full macOS Restore IPSW Firmware Files Database – Mr. Macintosh

Veertu's Anka

这个是我目前最推荐的一种方法,所以放在第一个说。

Anka 是什么?根据官网的介绍,Anka 是一个专门用来管理 macOS 虚拟机的软件,可以与现有的基于容器的 DevOps 工作流集成,为 iOS 应用的构建与测试提供 CI/CD 自动化支持。再看下其开发者 Veertu,也是做 iOS CI 和 macOS 云这一块的。

并且今年十月发布的 Anka 3.0 (beta) 已经支持在 M1 Mac 上创建 macOS 虚拟机了,正是我们所需要的。

下载 Anka M1 beta 版,安装后打开,就可以直接通过图形界面创建虚拟机了:

anka-m1-beta

或者,你也可以使用命令行创建虚拟机(相关文档在这里):

anka create --ram-size 4G --cpu-count 4 --disk-size 80G \  --app ~/Downloads/UniversalMac_12.0.1_21A559_Restore.ipsw 'macOS 12'

运行虚拟机:

anka start -uv

Anka 默认将虚拟机存储在 ~/Library/Application Support/Veertu/Anka 目录下,可以参考这里修改保存位置,或者干脆做个软链接也行。虚拟机的配置文件也在同目录下的 config.yaml 文件中,有些图形界面不提供的配置项可以在这里修改。

也可以使用命令行修改,比如修改虚拟机的分辨率和 DPI:

anka modify 'macOS 12' display -r 2560x1600 -d 220

另外,Anka 提供的 Guest Tool 会自动打开虚拟机内 macOS 的自动登录、SSH 并且阻止系统休眠(应该都是为了自动化服务的),并且提供了剪贴板共享、anka cp 文件复制,以及可以直接在虚拟机内执行命令的 anka run 等功能。

anka-macos-vm

不过有一个需要顾虑的是 License 的问题,在 beta 期间可以免费使用 Anka 没问题,但不知道正式版发布以后如何。不过原本 Veertu 家面向个人开发者的 Anka Develop 就是免费的,所以或许并不需要担心。

或者,你也可以使用本文最后提到的开源方案,体验也是不错的。

Parallels Desktop

毕竟是 Mac 虚拟机行业名声最响的,其实我第一个想到的也是 PD。

查了一下,macOS 作为 Guest OS 是 PD17 才支持的功能(前略,天国的 PD16 用户),然后 17.1 更新添加了 Parallels Tools 的支持,还提了一嘴「虚拟机默认磁盘大小从 32 GB 增加至 64 GB」。

我最开始还不知道这有什么好拿出来说的,后来才知道原因:你在 PD 中甚至无法调整 Mac 虚拟机的磁盘大小。不仅是磁盘,CPU 核心数、内存大小、网络连接方式都不能改,可配置项为零(至少无法在图形界面中配置),完完全全就是个半成品。

如果你确实想安装,这里是官方教程:Install macOS Monterey 12 virtual machine on a Mac with Apple M1 chips

点「新建虚拟机」以后,安装助手里就有直接下载 macOS 的选项。看起来很友好,然而……

pd-vm-installation-failed

啃哧啃哧下载了半天,最后提示「安装系统时出错」,也不知道为什么。查了下官方 Knowledge Base,貌似也不是个例:Inability to create a macOS Monterey 12 VM on Mac computers with Apple M1 chips

后来我找到了这篇文章:Customizing MacOS guest VMs in Parallels 17 on Apple Silicon,按照其中的介绍,通过命令行创建虚拟机,竟然就可以运行了……

/Applications/Parallels\ Desktop.app/Contents/MacOS/prl_macvm_create \  ~/Downloads/UniversalMac_12.0.1_21A559_Restore.ipsw \  /Volumes/xxx/Parallels/macOS\ 12.macvm \  --disksize 80000000000

我之前用 17.0.1 版本的时候也尝试用 prl_macvm_create 创建虚拟机,但是在进度到 90% 的时候失败了,提示「内部虚拟化错误。安装失败」。升级到 17.1.0 后虽然安装助手还是「安装系统时出错」,但命令行是可以正常创建虚拟机的。

命令行启动虚拟机:

/Applications/Parallels\ Desktop.app/Contents/MacOS/Parallels\ Mac\ VM.app/Contents/MacOS/prl_macvm_app \  --openvm /Volumes/xxx/Parallels/macOS\ 12.macvm

安装完成后,在 PD 控制中心可以导入 .macvm 格式的虚拟机文件,导入以后就可以从图形界面启动了。

pd-macos-vm

作为一个商业虚拟机软件,且不说快照、Suspend,连最基本的 VM 管理功能都欠奉,我也是无话可说了。想知道还有哪些功能是目前还不能用的,可以查看 Known issues and limitations

MacVM

MacVM 是一个开源项目,基于 Virtualization framework(当然啦,大家都是用的这个),提供了简单的图形界面用于配置虚拟机。

因为作者并没有提供编译好的程序,所以需要自己使用 Xcode 从源码编译。

下载源码,用 Xcode 打开 MacVM.xcodeproj,在 Signing & Capabilities 中修改为自己的开发者证书:

macvm-xcode

点击运行,会跳出来一个文件选择框,不用管先叉掉。

然后菜单栏 File -> New,新建虚拟机。输入 CPU 核心数、内存和磁盘大小后点菜单栏 File -> Save 保存,会生成一个 .macosvm 包。之后虚拟机的虚拟磁盘镜像也会保存在这个 bundle 中,所以要留意选择保存的位置。

macvm-new-vm

然后点 Select IPSW and Continue 按钮,选择之前下载的镜像文件,点 Install,等它安装完就好了。(最开始的版本还要自己生成磁盘镜像,然后拷贝到应用容器中,还要用 Apple Configurator 2 手动装系统,相比起来现在已经友好很多了)

安装完成后,窗口会整个儿变黑,此时就可以点右上角的启动按钮启动虚拟机了。

macvm-success

用这种方法优点是开源,有啥不爽的都可以自己改,包括没有提供配置项的地方。缺点就是要自己编译,毕竟不是谁都装了 Xcode 的。

跑起来以后和上面两种基本没差别,因为实际的虚拟机创建、安装和运行都是 Virtualization framwork 实现的,整个项目的代码其实并不多。

GitHub 上还有一些类似的项目,这里也列出来供参考:

最后

以上三种方法,其实底层大家都是一样的,就看在此之上谁做得更完善了。综合来看,目前感觉 Anka 的使用体验是最好的。

关于 M1 Mac 运行 macOS 虚拟机的一些参考链接:

UTM 有一个 dev-monterey 分支,我还没有尝试,不知道以后会不会推出支持 macOS Guest 的版本。

听说还有人使用 OSX-KVMDocker-OSX 跨架构在 M1 上运行了 x86 的 macOS,但是性能很糟糕(simulation 嘛)。

另外,以上的这些虚拟机方案都不支持快照恢复,有点麻烦。不过好在我用来放虚拟机的移动硬盘是 APFS 格式的,支持写时复制 (Copy on write),所以直接把镜像整个儿复制一份就好了,很快,也不会占用多余的存储空间。

GitHub 全家桶:Actions 自动构建多架构 Docker 镜像并上传至 Packages (ghcr.io)

By: prin
24 November 2021 at 18:30

前段时间把 GitHub 的用户名修改成了 @prinsss,准备把其他地方的账号也修改一下的时候,却发现 Docker Hub 的 username 不能改,只能砍掉重练(npm 也是)。

想想反正我 Docker Hub 上也没上传什么东西,不如就用 GitHub 自家的 Container registry 来托管镜像吧!

这里有个小插曲:其实我挺早之前就想要改名了,但当时在忙秋招,考虑到改名后可能会有一些后续要处理(擦屁股),所以只是创建了一个 organization 把名字占住,等有时间了再正式改名。然而后来我把组织删了,想要修改 GitHub 账户的用户名时,却提示 prinsss 这个名称 unavailable(我确定它是没被占用的,因为我还能再用这个名字创建组织),不知道是不是触发了内部的什么保留机制。

最后还是发工单找客服解决了,而且等了一个多星期才回复,也是挺无语的。原来的 printempw 这个名字我也保留了,所以 printempw.github.io 这个域名还是可以访问的,目前是两边同步更新,后续再慢慢迁移。

GitHub Packages 介绍

其实最开始知道这个还是因为 Homebrew,看它每次安装软件下载 bottle 时都会从 ghcr.io 这个域名下载。好奇去查了一下,发现原来 GitHub 自己也整了一个软件包仓库,颇有一统天下的味道。

GitHub Packages 支持托管 Docker、npm、Maven、NuGet、RubyGems 等软件包,用起来比较像私有库。比起官方 registry 的好处就是其与 GitHub 完全集成,可以把源代码和软件包整合在一起,包括权限管理都可以用 GitHub 的那一套。

GitHub Packages 对于开源项目完全免费,私有仓库也有一定的免费额度

手动上传镜像

基础用法和 Docker Hub 是一样的,只是 namespace 变为了 ghcr.io。

首先创建一个 PAT (Personal Access Token) 用于后续认证:

  • 打开 https://github.com/settings/tokens/new?scopes=write:packages
  • 创建一个 PAT,勾选 write:packages 权限

注意:如果是在 GitHub Actions 中访问 GitHub Packages,则应该使用 GITHUB_TOKEN 而非 PAT 以提升安全性。后续章节会说明如何在 Actions 中使用 GITHUB_TOKEN

然后我们就可以用这个 Token 登录镜像仓库了:

export CR_PAT=YOUR_TOKENecho $CR_PAT | docker login ghcr.io -u USERNAME --password-stdin

尝试一下推送镜像:

docker tag hello-world:latest ghcr.io/prinsss/hello-world:latestdocker push ghcr.io/prinsss/hello-world:latest

可以看到已经出现在 GitHub 上了:

packages-hello-world

刚上传的镜像默认都是 private,可以在 Package Settings 下方的 Change package visibility 处修改为公开镜像。

自动构建并上传

连镜像都放 GitHub 上了,那怎么好意思不用 GitHub Actions 呢!

下面就使用 Actions 实现代码更新后自动构建多架构 Docker 镜像,打 tag 并发布。

废话不多说,直接贴配置:

# yaml-language-server: $schema=https://json.schemastore.org/github-workflowname: Build Docker Image# 当 push 到 master 分支,或者创建以 v 开头的 tag 时触发,可根据需求修改on:  push:    branches:      - master    tags:      - v*env:  REGISTRY: ghcr.io  IMAGE: prinsss/ga-hit-counterjobs:  build-and-push:    runs-on: ubuntu-latest    # 这里用于定义 GITHUB_TOKEN 的权限    permissions:      packages: write      contents: read    steps:      - name: Checkout        uses: actions/checkout@v2      # 缓存 Docker 镜像以加速构建      - name: Cache Docker layers        uses: actions/cache@v2        with:          path: /tmp/.buildx-cache          key: ${{ runner.os }}-buildx-${{ github.sha }}          restore-keys: |            ${{ runner.os }}-buildx-      # 配置 QEMU 和 buildx 用于多架构镜像的构建      - name: Set up QEMU        uses: docker/setup-qemu-action@v1      - name: Set up Docker Buildx        id: buildx        uses: docker/setup-buildx-action@v1      - name: Inspect builder        run: |          echo "Name:      ${{ steps.buildx.outputs.name }}"          echo "Endpoint:  ${{ steps.buildx.outputs.endpoint }}"          echo "Status:    ${{ steps.buildx.outputs.status }}"          echo "Flags:     ${{ steps.buildx.outputs.flags }}"          echo "Platforms: ${{ steps.buildx.outputs.platforms }}"      # 登录到 GitHub Packages 容器仓库      # 注意 secrets.GITHUB_TOKEN 不需要手动添加,直接就可以用      - name: Log in to the Container registry        uses: docker/login-action@v1        with:          registry: ${{ env.REGISTRY }}          username: ${{ github.actor }}          password: ${{ secrets.GITHUB_TOKEN }}      # 根据输入自动生成 tag 和 label 等数据,说明见下      - name: Extract metadata for Docker        id: meta        uses: docker/metadata-action@v3        with:          images: ${{ env.REGISTRY }}/${{ env.IMAGE }}      # 构建并上传      - name: Build and push        uses: docker/build-push-action@v2        with:          context: .          file: ./Dockerfile          target: production          builder: ${{ steps.buildx.outputs.name }}          platforms: linux/amd64,linux/arm64          push: true          tags: ${{ steps.meta.outputs.tags }}          labels: ${{ steps.meta.outputs.labels }}          cache-from: type=local,src=/tmp/.buildx-cache          cache-to: type=local,dest=/tmp/.buildx-cache      - name: Inspect image        run: |          docker buildx imagetools inspect \          ${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ steps.meta.outputs.version }}

自动构建的效果可以在我的 GitHub 上查看(其实就是之前写的那个 使用 Google Analytics API 实现博客阅读量统计)。

另外有几个需要注意的点:

上传时出现 400 Bad Request

这个昨天搞得我真是一脸懵逼,报错是这样的:

#16 exporting to image#16 pushing layers 0.5s done#16 ERROR: unexpected status: 400 Bad Request------ > exporting to image:------error: failed to solve: unexpected status: 400 Bad RequestError: buildx failed with: error: failed to solve: unexpected status: 400 Bad Request

排查了好久,最后发现是我打 tag 的时候忘记加上用户名了,原本是 ghcr.io/prinsss/ga-hit-counter 的,我给打成了 ghcr.io/ga-hit-counter,难怪推不上去(也要吐槽一下这个报错,就一个 400 鬼知道是什么啊)。

上传时出现 403 Forbidden

把上面那个解决了以后,心想这次总该成了吧,结果又来了个 403,我又一脸懵逼:

#16 exporting to image#16 pushing layers 0.7s done#16 ERROR: unexpected status: 403 Forbidden------ > exporting to image:------error: failed to solve: unexpected status: 403 ForbiddenError: buildx failed with: error: failed to solve: unexpected status: 403 Forbidden

再一番排查,发现是需要在 Package Settings 中的 Manage Actions access 处指定可以访问该软件包的源码仓库(也就是 Actions 所在的仓库)。好吧……

manage-actions-access

添加了仓库,这下确实可以了。

元数据自动生成

docker/metadata-action 这是一个比较有意思的 action,它可以从源码以及触发构建的 event 中获取数据,自动生成相应的 Docker 镜像 tag 以及 label。(在 GitHub 文档官方的示例中,这是由一段脚本完成的)

比如默认的效果就是:

EventRefDocker Tags
pull_requestrefs/pull/2/mergepr-2
pushrefs/heads/mastermaster
pushrefs/heads/releases/v1releases-v1
push tagrefs/tags/v1.2.3v1.2.3, latest
push tagrefs/tags/v2.0.8-beta.67v2.0.8-beta.67, latest

也就是我现在使用的:源码推送到 master 分支则自动构建并更新 master tag 的镜像;在 git 上创建以 v 开头的 tag,Docker 那边也会自动创建相应的 tag 并且更新 latest,不错不错。(不过想想我可能保留一个 tag 触发就够了)

比如我 push 了一个 v0.2.0 的 tag 上去,自动生成的元数据就是这样的:

Run docker/metadata-action@v3Context info  eventName: push  sha: 6071f564087d49be48dc318b89fc22ff96cf6a17  ref: refs/tags/v0.2.0  workflow: Build Docker Image  action: meta  actor: prinsss  runNumber: 11  runId: 1495122573Docker tags  ghcr.io/prinsss/ga-hit-counter:v0.2.0  ghcr.io/prinsss/ga-hit-counter:latestDocker labels  org.opencontainers.image.title=google-analytics-hit-counter  org.opencontainers.image.description=Page views counter that pulls data from Google Analytics API.  org.opencontainers.image.url=prinsss/google-analytics-hit-counter  org.opencontainers.image.source=prinsss/google-analytics-hit-counter  org.opencontainers.image.version=v0.2.0  org.opencontainers.image.created=2021-11-23T14:10:35.953Z  org.opencontainers.image.revision=6071f564087d49be48dc318b89fc22ff96cf6a17  org.opencontainers.image.licenses=MIT

如果想要修改为其他方案,action 也提供了丰富的配置项,可以自行修改。

最后

用 GitHub Packages 托管 Docker 镜像,体验还是挺不错的。硬要说有什么缺点的话就是不好配置国内镜像吧,毕竟大部分国内镜像都是对应 Docker Hub 的。

另外多架构镜像的这个构建时间也是挺感人,模拟 arm64 一次要六七分钟,哈人。(所以写 Dockerfile 还是挺讲究的,怎么让缓存效率最大化,这方面还得再学习)

参考链接:

题外话,秋招后这段时间我也折腾了一些东西,有空慢慢发出来吧。

使用 ESLint + Prettier + Commitlint 规范代码风格与提交流程

By: prin
14 October 2021 at 05:37

最近因为课程需要开了几个多人协作的新项目,感觉有必要在团队中强制一下代码规范,免得提交上来的东西对 leader 血压不好。前后端都是 TypeScript 的,所以就用流行的 ESLint + Prettier 组合拳(配合 Standard 规范),EditorConfig 同步编辑器配置,再加上 commitlint 规范提交信息,最后用 Git Hooks 实现自动化检查。

配置虽然不难,但还是有点繁琐的,所以记录一下,如果忘了下次可以翻回来看。

简介

首先来介绍一下这些工具都是啥吧。

ESLint 是一个插件化并且可配置的 JavaScript 语法规则和代码风格的检查工具,能够帮你轻松写出高质量的 JavaScript 代码。

简单来说就是可以静态分析代码中的问题(包括语法问题和代码风格问题,比如未使用的变量,if 和括号之间没有空格),给出提示,并且能够自动修复。

Prettier 是一个“有态度” (opinionated) 的代码格式化工具。它支持大量编程语言,已集成到大多数编辑器中,且几乎不需要设置参数。

什么叫 opinionated?不同于 ESLint 什么都让你配置,Prettier 是一种严格规范的代码风格工具,主张尽量降低可配置性,严格规定组织代码的方式。

Prettier 只提供少量配置项,这么做的原因很简单:既然为停止争论而生,那么为何还需要设置更多选项让人们继续纠结于如何配置呢?

规矩就是这样,不服憋着。

检查你的 Git 提交信息是否符合配置的格式要求。

相信大家或多或少都见过某些人奔放不羁的 commit message,不仅给项目管理带来困难,看着也挺难受的。使用 commitlint 可以实现在提交前检查提交信息是否符合规范,配合 commitzen 食用更佳。


看到这里你可能有些疑问,ESLint 可以自动修复代码风格问题,Prettier 也可以格式化代码,那它们两个不会打架吗?没错,确实会有冲突的情况,而这也是我们后面要解决的。

既然会冲突,那为什么要同时使用它们呢?主要有这几个原因:

  • Prettier 的代码格式化能力更强。它的工作原理是把代码解析成 AST,然后根据规则重新输出,等于帮你整个儿重写了一遍代码。ESLint 的 --fix 自动修复虽然也可以实现一定程度的代码格式化,但没有 Prettier 效果好。
  • Prettier 支持的文件格式更多,比如 HTML、CSS、JSON、Markdown 等等。

当然,如果 ESLint 对你来说已经够用,那么不加入 Prettier 其实也是完全没问题的。

ESLint

这里我们采用 Standard 规范。

以 Vite 新建的 Vue 3 + TS 白板项目为例:

pnpm add -D \    @typescript-eslint/eslint-plugin \    @typescript-eslint/parser \    eslint \    eslint-config-standard-with-typescript \    eslint-plugin-import \    eslint-plugin-node \    eslint-plugin-promise \    eslint-plugin-vue

如果你的项目不使用 TypeScript,可以把 eslint-config-standard-with-typescript 规则替换为 eslint-config-standard

"lint": "eslint \"src/**/*.{vue,ts,js}\" --fix"

.eslintrc.js

module.exports = {  parser: 'vue-eslint-parser',  parserOptions: {    parser: '@typescript-eslint/parser',    project: './tsconfig.json',    extraFileExtensions: ['.vue']  },  extends: [    'plugin:@typescript-eslint/recommended',    'standard-with-typescript',    'plugin:vue/vue3-recommended'  ],  root: true,  env: {    node: true  },  rules: {    'vue/script-setup-uses-vars': 'error',    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',    '@typescript-eslint/explicit-function-return-type': 'off',    '@typescript-eslint/explicit-module-boundary-types': 'off',    '@typescript-eslint/no-explicit-any': 'off',    '@typescript-eslint/strict-boolean-expressions': 'off'  },  globals: {    defineProps: 'readonly',    defineEmits: 'readonly',    defineExpose: 'readonly',    withDefaults: 'readonly'  }}

tsconfig.json

{  "compilerOptions": {    "target": "esnext",    "useDefineForClassFields": true,    "module": "esnext",    "moduleResolution": "node",    "strict": true,    "jsx": "preserve",    "sourceMap": true,    "resolveJsonModule": true,    "esModuleInterop": true,    "lib": ["esnext", "dom"]  },  "include": [    "src/**/*",    ".eslintrc.js",    "vite.config.ts"  ]}

Prettier

需要注意的是,Prettier 和 Standard 规范并不完全兼容。

pnpm add -D \    prettier \    eslint-config-prettier \    eslint-plugin-prettier

.eslintrc.js

 extends: [   'plugin:@typescript-eslint/recommended',   'standard-with-typescript',   'plugin:vue/vue3-recommended',+  'plugin:prettier/recommended' ],

.prettierrc

{  "trailingComma": "none",  "semi": false,  "singleQuote": true}
"lint": "eslint \"src/**/*.{vue,ts,js}\" --fix","format": "prettier --write \"src/**/*.{vue,ts,js}\""

EditorConfig

自古以来,Tab or Space 就是不曾停歇的圣战。

不同成员都有不同的喜好,使用的编辑器/IDE 也不尽相同。那么为了 codebase 的规范,在所有项目成员中使用一个统一的配置是很有必要的。

root = true[*]charset = utf-8indent_style = spaceindent_size = 2end_of_line = lfinsert_final_newline = truetrim_trailing_whitespace = true[*.{js,jsx,ts,tsx,vue}]indent_style = spaceindent_size = 2trim_trailing_whitespace = trueinsert_final_newline = true

Commitlint

比如很多项目都采用的 Conventional Commits 就要求提交信息必须符合以下规范:

<type>[optional scope]: <description>[optional body][optional footer(s)]

为什么要使用 Conventional Commits?

  • 自动生成 CHANGELOG
  • 基于提交类型生成语义化版本号
  • 项目提交历史更清晰

Git Hooks

【鸽了】

反正就是让上面那些工具可以在 Git 提交时自动执行,检查不通过的就打回。

最后

还记得很久以前别人给我发了个 Pull Request,我一看,发现有好多地方的代码风格都和我不一样,比如单双引号、分号的使用,还有我最不能忍的 if(xxx){} 之间不加空格……

但我想想再叫人家改也怪麻烦的,就默默接受了 PR,然后再默默改成自己的代码风格……

现在有了这些东西工作流程就规范多了:

  • 你乱写也行,我直接给你格式化掉;
  • 语法检查,在编写过程中就排除潜在的 BUG;
  • 提交上来的代码必须通过以上验证,不然就拒绝;
  • 提交信息也要规范,不能瞎写乱写。

当然了,规矩是死的人是活的,这一套下来也没法保证一定万无一失。不过相比以前群魔乱舞的场面,已经省心了不少。

不过说实在话,比起配置这些工具,推行一个大家都能接受的规范才更难吧(x)

❌
❌