浅羽的主要手機是 Hisense A5 Pro,它主打一塊電子紙螢幕,在戶外有絕佳的可讀性。對於浅羽來說,不出門時則將它當作傻瓜機(dumb phone)使用,大部分時候無需理會它;而出門在外時,它可以滿足最基礎的聯絡、地圖和支付需求,並且充電一次可以使用 3-4 天,無需特別計劃出門時間然後提前充好電。
截至文章發表,浅羽使用 A5 Pro 已經近一年,更覺得它就是手機做爲工具應該有的樣子,也分享一下使用心得。
TL; DR:
說到 E-Ink 手機,海信並非是第一次做了:
回到 A5 Pro ,它外觀非常樸素,官方提供有黑、白、墨綠三色,浅羽則是選擇白色。手機背面是細磨砂的塑料材質配上一顆鏡頭、一顆補光燈,中間(相比前代)增加了一枚指紋辨識器,沒有高級材料、沒有花哨暗紋,回歸了最原始的智慧型手機的感覺。整面則是主打的一片單色 E-Ink 顯示器,沒有異形設計、沒有窄邊框,只有出廠就貼好的一張磨砂保護貼。浅羽自己是十分喜歡這樣的感覺。
針對這片 E-Ink,A5 Pro 在系統中做了許多調整。
首先下方增加了一個顯示控制面板,提供四種顯示模式可選,分別是清晰、均衡、順暢、極速:「清晰」模式是四者中解析度最好的模式,但是畫面的反應速度卻是最慢;「均衡」是對比最高的一個模式,有時它的可看度甚至比「清晰」還要好,這也是浅羽最愛用的模式;而使用「極速」則可以看 GIF 動畫甚至一些影片,當然前提是如果真的有需要在 E-Ink 上查看動態內容的話。四個模式可以隨時切換,也可以在系統設定中針對不同應用自動切換不同模式,同時還可以設定使用輸入法時自動切換到「極速」模式。其次是補光燈部分,除了自動和手動亮度外,還提供了「關閉」和「黑夜」(即超低亮度)兩個選項,只可惜受限於硬體未有提供雙色溫前光燈。
系統內建的應用都使用了高對比度的黑白框線風格圖示,同時針對(部分)第三方應用有覆蓋同樣風格的圖示。因應 E-Ink 的特性,系統介面本身也大量使用白底黑字的高對比度風格,並且內建應用都去除了過渡動畫。可惜的是,動畫去除得並不徹底,比如輸入 SIM 卡 PIN 碼、使用 NFC 配對藍芽時依然還有一些動畫,同時一些第三方應用也會固執地顯示華麗的動畫,並在螢幕上留下一團團殘影。本質上,海信並沒有完全重新爲 E-Ink 設計一套新系統,而是在儘可能保留原先 Android 的 UX 的前提下改造 UI,使其更適合 E-Ink 顯示──比如說,擷取螢幕就可以發現,很多的圖示依然是彩色的;相機拍出的照片也依然是彩色的。
海信官方對 A5 Pro 的定位是一款「閱讀手機」,因此也提供了許多輔助功能試圖改善閱讀體驗。比方說專門的「閱讀模式」,打開後可以延後通知、降低鈴聲音量;「單頁捲動模式」,開啓後會在捲動完成手指鬆開時才一併刷新畫面。可惜的是,浅羽幾乎沒有測試出有什麼應用程式中可用。全局音量鍵翻頁是沒有的,不過解決翻頁問題還可以選擇啓用「滑動指紋辨識器翻頁」;這個在 Pixel 上被浅羽不屑一顧的功能,現在成爲了在第三方應用程式中翻頁的最佳方式。在指紋辨識器上上下滑動,也可以做到一次翻一頁的效果;雖然應用程式內還是會顯示動畫,但是翻頁距離固定,效果會比觸摸滑動好一些,不過無法支援橫向滑動。直接觸摸指紋辨識器但不滑動,還會觸發手動的全域刷新,這在使用 KOReader 等閱讀器時非常實用,尤其是考慮到 A5 Pro 即使在「清晰」模式下全域刷新也很不主動。
爲了發揮 E-Ink 雙穩態的特性,A5 Pro 還導入了「截屏便籤」功能,可以在鎖定螢幕後甚至關機後繼續顯示螢幕擷取畫面,用來顯示名片一類的也很合適。如果沒有需要顯示的便籤,也可以選擇內建的隨機詩詞、單詞作爲鎖定螢幕,每種鎖定螢幕也有一些設定可以調整,客製化程度較高。或者乾脆在鎖定螢幕上顯示大時鐘,這樣就可以直接放在桌面當擺件了。關機以後亦可以保持顯示特定的圖片。當然,也可以選擇關閉自動鎖定螢幕,保持顯示某一個應用程式,也不會額外耗費多少電力。
除了閱讀,海信 A5 Pro 主推 Hi-Fi 音樂功能。作爲 E-Ink 下比較合適的娛樂方式,海信在硬體方面使用了一片 AKM AK4377AECB 作爲 DAC,同時還給了很多連線的可能性:NFC 和支援 LDAC 的藍牙功能,保留 3.5mm 耳機接口,同時 USB Type-C 也可以接駁數位音訊裝置,搭配大部分耳機都不是問題。不過聲音質素則只能稱一般般,內建的 Hi-Fi 模式開啓後也只是覺得音量更大了(或許是推力更大了?)。
內建的音樂應用程式也頗爲實用,播放浅羽音樂庫中的 AAC 和 FLAC 檔案都沒有問題,可以根據標籤分類選歌、顯示歌詞,不過尚缺少 ReplayGain 功能。得益於手機不怎麼樣的效能,播放音樂時續航表現也令浅羽安心。打開音樂程式、隱藏導航列、插上耳機,這就是一支簡單好用的隨身聽。
海信 A5 Pro 在其他硬體的規格上都中規中矩(甚至可以說是落後),但可用度都不低。前後兩枚鏡頭正如官方宣傳的「前置識別,後置掃碼」,搭配可以用來做支付認證的指紋辨識,基本上做到了安全和方便兼顧。不過遺憾的是僅僅採用了轉子馬達。習慣了不錯的線性馬達之後,振動觸感真的非常差。不過倒也不是墊底──畢竟還有採用了線性馬達、但觸感更差的小米 Civi。
比較有特色的是左側是「墨智鍵」,支援自訂短按、雙擊兩種操作,可惜的是長按只能選擇是否使用語音助手。浅羽把短按設定成全局刷新(如果畫面太髒了可以按一下),點按兩次設定成清晰/流暢模式切換。另外,「墨智鍵」和右側的音量鍵同時按下,可以開啓或者關閉前光,不小心在深夜關掉了前光時尤爲實用。同時系統也支援在來電時一律開啓前光,避免低光狀況下的手忙腳亂。
一旦習慣了 E-Ink 的缺點,就可以盡情享受 E-Ink 帶來的優勢。
E-Ink 的陽光下可讀性在戶外非常實用,這使得 A5 Pro 在外出時是非常好用的工具。選用「清晰」模式顯示地圖,抑或是選用「流暢」模式查看和回覆訊息,自然光照成爲了優勢而非影響閱讀的阻礙;而且可以保持顯示地圖或音樂等等,而無需太擔心耗電問題。在展示條碼的場景下,E-Ink 同樣好用;不過對於非雷射讀取的條碼辨識器來說,可能需要打開前光輔助辨識。
最大的問題是充電:習慣了新近機種的快速充電後,會覺得給 A5 Pro 充電很慢很慢;好在受惠於不算高的配置和 E-Ink,充電一次能夠使用 3-4 天,勉強讓充電速度不再是大問題。浅羽乾脆貼了無線充電貼,有空就用無線慢慢充電,還能順便當作桌面座鐘。
總的來說,這支手機足以被稱爲好用的工具。其他的方面——就交給更大的螢幕、甚至其他的媒介吧!
]]>過年的時候去 partner 家。本來的計畫是:早上去菜市買好新鮮食材,下午開始準備年夜飯,這樣吃完正餐還可以喝點酒。家裏吃飯的人多,只算大人的話也有八張嘴;但是每人都有一些愛吃或者不吃的食材,所以買菜的時候自然想着多買幾樣。魚、蝦、花螺,加上祭拜用完的雞和一些其他的青菜,也足夠吃好一餐了。結果早早去買了菜,去到家裏,都要等到點了祭祀。午餐隨便糊弄了一下,等到下午,終於開始討論晚上(年夜飯)的菜單了。本來訂好四樣大菜,突然長輩說「晚上不要吃花螺」,追問原因又支支吾吾不說,只說不要吃。不讓吃也好吧,可是提前買的時候不說;現在買完了,海鮮不做預處理也不能久放。還好帶了兩瓶好酒,於是趁時間還早,先做了一碟酒煮花螺,當下酒菜吃(聽起來好像用酒下酒)。
下午的時光倒是還挺開心,有一些輩份近、年齡也相仿的表哥表弟來走動。他們平日不缺好酒好肉,但是聽說有不太常見的日本酒和下酒菜,倒也都小酌幾口。期間也終於陸陸續續開始準備祭祖用的茶、酒、果品、熱食,但最重要的一樣──米飯──卻遲遲不好。約摸又過了半個鍾,米飯才煮好,然後才能正式開始祭拜。淺羽本來以爲這個過程應該也要花些時間,就先去處理食材了。沒成想,幾個小時的等待,結果祭拜 10 分鐘就結束了!聊到晚上的安排,突然又說想 6 時吃飯,但是這個時候已經接近下午 4 時,家庭廚房又缺鍋竈,做菜只能一道一道做,難免時間有些緊迫。
緊趕慢趕,最後終於在六點半時做好了全部的菜品。菜端上桌,結果連一桌人都坐不整齊。大人有的忙着想先把廚具清理趕緊,有的不緊不慢地喝着茶滑着手機,有的在追着不想吃飯的小孩餵飯……難得有坐上桌子的同齡人,但是一直也是鬱鬱寡歡。到頭來,一桌飯菜,其他人稀稀拉拉地來、沒動兩筷子又走了。沒有想像中熱鬧的飯桌,只有就浅羽和 partner 在努力進食。
大人們這麼着急當然也是有原因的──他們急着結束這一天,所以吃飯也只是草草了事,又不停地問着「吃好了沒有」。大家都有點心不在焉,只想著趕緊把年過完,完全忘記了節日歡聚的意義。再好的飯菜,如果趕着時間,也索然無味了。
]]>首先使用光猫背面信息,登录普通用户管理界面。再修改浏览器地址,访问:http://192.168.1.1/usr=CMCCAdmin&psw=aDm8H%25MdA&cmd=1&telnet.gch,浏览器会显示 Success。
此时使用 telnet 登录光猫,用户名为 CMCCAdmin
,密码为 aDm8H%MdA
。
登录光猫以后,虽然我们还是没有办法获得超级密码,但是我们可以修改超级密码:
# 修改超级密码为 admin
sidbg 1 DB set DevAuthInfo 0 Pass admin
# 保存设置
sidbg 1 DB save
此时再回到光猫登录页面,使用用户名 CMCCAdmin
与密码 admin
即可登录管理员账户。
]]>参考链接:https://www.bilibili.com/read/cv15548233
NiceHCK 是福兰声的经销商,闹掰了以后福兰声才出来自立门户的。所以 NiceHCK 前期卖的和后期卖的应该不是同一个作坊出来的。
其實浅羽一直想感受流下「悔恨之淚」的感覺,最近偶然又看到了透明粉色配鍍銀線的款式,就當交「粉紅稅」買下了 3.5mm 無麥版。
經過這幾年的發展,這款耳機不僅包裝不再簡陋,甚至還擁有了耳機娘「原道醬」的插畫形象。除開有點精美味道的紙盒外,包裝裏甚至還有一張原道醬的卡片。除此之外還給了一黑一白兩對實心海綿套,以及證明自己不是三無少女產品的合格證和保證書。
耳機本身當然就是 MX500 的公模沒得跑的。透明粉色的顏色實際上很偏玫紅,不過浅羽原本以爲戴着會很顯眼,實際上全部被側髮遮住了完全看不到(笑)。這麼看,長髮的話其實買什麼顏色都無所謂,尤其是不用擔心自己喜歡的顏色戴起來合不合適。鍍銀線看起來就很有 Hi-Fi 的感覺,據說透明線是新版,音質都會好一些。
按照慣例,曲庫依然是浅羽聽什麼就有什麼、環境依然是浅羽在哪裏就是哪裏,依然是不會有嚴謹的試聽,只有感受。因爲沒有配中空海綿套,所以浅羽直接用了自己的備件。本來接了 amp 想認真聽聽,後來覺得這麼做不知道是在侮辱誰。所以最後是用 xDuoo X3II 和 HiBy R2 + HiBy FD3 的組合。首先低頻的量是很大的,相對地高頻就被壓制了,整個聲音聽起來偏悶。不過繼續聽下去,中頻很飽滿、厚實,非常適合聽一些人聲和器樂。不過,整體的聲音還是很糊的;甚至不用過多的樂器,單一樂器只需要旋律複雜一些,聲音就會混雜成一團。比如一些鋼琴曲,編排較複雜的部分,低音就會混著中音,以致無法分辨。至於什麼聲場、定位,整體人聲不能說貼不貼耳,只能說聲音根本就在大腦中間,而伴奏是分佈在兩耳旁的——這個價格,聲音均勻就不錯了,對吧?
在這種偏下盤的氛圍中,聲音聽起來很溫暖,其實非常適合睡前聽聽催催眠;可惜浅羽長期佩戴這個造型的耳機還是會耳朵痛。另外,這樣的聲音,在馬路邊聆聽的時候,可以很好地保持低音部分不被車流的噪音掩蓋,聲音反而聽著更加「正確」。
如果要總結的話,YD30 絕對不能用來發燒入門,只能是有經驗了當個玩具笑一笑。但是相比其他類似的選擇呢?比它價格幾乎翻倍的潛韻 25 也並沒有更好,而更高價位的潛韻 39 則完全不是一個聲音走向。可以說,雖然大家的聲音都有各種各樣的缺陷,但百元以內,YD30 也可以稱得上的是物有所值了。加上相對多彩的配色和原道醬自己代言自己,倒也不妨一試。
原道醬的裙子有點似曾相識,浅羽好像也有一條類似的。不過這個乳量真是只能羨慕了……話說回來,如果裙子沒有口袋、原道醬的兩隻手也都沒有拿着的話,音源難道是綁在腿上的?
]]>假设你的域名为 example.com
,对应的证书为 example.com.crt
,私钥为 example.com.key
,DERP 对外提供服务的端口为 8443。
一行 docker 命令即可创建 DERP 服务器:
docker run -d \
--name derp \
--restart=always \
-p 0.0.0.0:3478:3478/udp \
-p 0.0.0.0:8443:8443 \
-v /path/to/example.com.crt:/app/certs/example.com.crt \
-v /path/to/example.com.key:/app/certs/example.com.key \
-e DERP_DOMAIN=example.com \
-e DERP_ADDR=:8443 \
-e DERP_CERT_MODE=manual \
fredliang/derper:latest
启动 3478 端口不要修改,8443 端口可以按照自己的喜好来改,DERP_ADDR 变量后面的端口好需要与之前的保持一致,但是这里的冒号不能丢。
接下来来到 Tailscale 管理页面 – Access controls 标签页中,将编辑区域的内容清空,并填入以下内容:
{
"derpMap": {
"Regions": {
"900": {
"RegionID": 900,
"RegionCode": "bj",
"RegionName": "Beijing",
"Nodes": [
{
"Name": "example.com",
"RegionID": 900,
"HostName": "example.com",
"DERPPort": 8443
}
]
}
}
}
}
RegionCode
与 RegionName
可以自定义,建议使用纯字母。DERPPort
需要与启动 docker 时设置的端口一致。点击保存之后即可。
在本地终端或者命令提示符中运行 tailscale netcheck
命令检查一下,看你设置的节点是否出现在下方的列表之中。
Report:
* UDP: true
* IPv4: yes, *.*.*.*:*
* IPv6: yes, [*:*:*:*:*:*:*:*]:*
* MappingVariesByDestIP: true
* HairPinning: false
* PortMapping:
* CaptivePortal: false
* Nearest DERP: Beijing
* DERP latency:
- bj: 36ms (Beijing)
- hkg: 235.7ms (Hong Kong)
- sea: 240.9ms (Seattle)
- sfo: 247ms (San Francisco)
- syd: 253.2ms (Sydney)
- sin: 259.5ms (Singapore)
- tok: 260.3ms (Tokyo)
- dfw: 271.5ms (Dallas)
- blr: 277ms (Bangalore)
- mia: 295.4ms (Miami)
- dbi: 314.6ms (Dubai)
- nyc: 315.2ms (New York City)
- tor: 326ms (Toronto)
- lax: 326.9ms (Los Angeles)
- den: 333.1ms (Denver)
- lhr: 347.6ms (London)
- hnl: 350.9ms (Honolulu)
- ord: 351.2ms (Chicago)
- sao: 407.1ms (São Paulo)
- fra: 487.8ms (Frankfurt)
- mad: 493.2ms (Madrid)
- par: 525ms (Paris)
- ams: 537.4ms (Amsterdam)
- waw: 555.3ms (Warsaw)
- jnb: 680.2ms (Johannesburg)
当使用 tailscale ping
命令去 ping 一个客户端的时候,就可以看到真正使用上了我们自己的节点。
pong from * (*.*.*.*) via DERP(bj) in 60ms
pong from * (*.*.*.*) via *.*.*.*:* in 58ms
如果你的云服务器的带宽、流量不是很大很多的话,做好客户端验证防止别人使用你的 DERP 服务器还是有用的。
首先在服务器上按照 Tailscale 官方安装手册安装客户端,在成功接入自己的局域网之后,本地会创建一个套接字接口 /var/run/tailscale/tailscaled.sock
,之后需要做的就是调整一下 docker 的命令:
docker run -d \
--name derp \
--restart=always \
-p 0.0.0.0:3478:3478/udp \
-p 0.0.0.0:8443:8443 \
-v /path/to/example.com.crt:/app/certs/example.com.crt \
-v /path/to/example.com.key:/app/certs/example.com.key \
-v /var/run/tailscale/tailscaled.sock:/var/run/tailscale/tailscaled.sock \
-e DERP_DOMAIN=example.com \
-e DERP_ADDR=:8443 \
-e DERP_CERT_MODE=manual \
-e DERP_VERIFY_CLIENTS=true \
fredliang/derper:latest
调整之处为两个,一个是将套接字接口挂载进容器中,另一个是添加环境变量 DERP_VERIFY_CLIENTS
,这样 DERP 就会验证连接的客户端是否与本机的客户端为同一个账号下,从而避免其他客户端白嫖服务器。
這一代 ThinkPad X1 Carbon 有 1.13kg 重,比起前兩代略爲重,但比起 Gen 6 來說基本是一致的。這比起 LG Gram 來說當然還有一定距離,但浅羽也可以舉出一個反面例子:搭配智慧型摺套連鍵盤(Smart Keyboard Folio)的 12.9 吋 iPad Pro(實測重量爲 624g + 413g ≈ 1.3kg)。
外觀上差別也不大,不過終於不是反貓咪的吹爪爪設計了!這一代的出風口放在了轉軸下,還因此而放棄了 one hinge 設計。同時還在 D 面設計了一條膠墊來提高筆電平放時的傾斜度。B、C 面 180 度開合的傳統藝能也是同樣保留的,不過由於底部墊高的存在,所以下半部分始終會保持一定的傾斜度。螢幕方面,尺寸維持 14 吋的前提下比例變成了 16:10,下巴的長短終於沒有那麼驚爲天人了。比例的變更加上窄邊框設計也使得外觀上更偏方正──當然,和 4:3 時代的飯盒比不了。這代的 Think Shutter 可以和 IR/RGB 共存了。就是不知道什麼時候可以有 1080P 的攝影機,這對於 video call 的幫助還是很大的。
開機做一些設定,發現 UEFI 新增了圖形介面,也支援滑鼠操作;但是浅羽不喜歡,好在還可以換回 Simple Text 模式。鍵盤方面,按鍵高度方面是有肉眼可見的降低,但是神奇的是沒有感受到明顯的鍵程變化,打鍵感維持了相對一致。Think Point 沒有變化。觸控板變得更寬了,連帶着觸控板的左右按鍵也變得更長了。不過這一代的觸控板在 Windows 下竟然無法靠內建驅動程式驅動,而沒有驅動程式的狀況下完全不能使用!如果不是熟練使用各種小紅小藍小白點的老用家,恐怕要頭痛了。倒是 Fedora 下無需額外設定可以直接使用。指紋辨識整合到了電源鍵上,同時電源燈號也充當了指紋辨識的提示燈號,同時擁有白、綠、黃三種顏色。指紋辨識的靈敏度比較一般,浅羽基本上都需要嘗試兩三次才能成功辨識。
效能方面,浅羽沒有詳細測試。新平臺有 4×2 個框框,加上較高的主時脈,應付日常的圖片後製自然是沒問題的。內建的 Iris® Xe 應付一些簡單的遊戲也是可以的,中間 partner 拿去玩了幾天 Europa Universalis IV,解析度設定在 4K 還是無法流暢運行,降到 1080P 後基本就沒有問題了。當然畢竟不是「遊戲本」,就不要認真和隔壁 Ryzen 去比了。滿載的風扇聲音也不小,但是比起 Gen 6 的高頻噪音來說還是略好,沒有刺耳的高頻,中頻飽滿,更加豐滿悅耳一些。
揚聲器意外地很不錯,這一代配備了 4 揚聲器,C 面兩個 0.8W 揚聲器,D 面還有兩個帶音腔的 2W 揚聲器。高音不甜,中音勉強準,低音也完全不沉;但人聲交談的話總算是勉強可以聽的。
總體來說,這代除了 16:10 顯示器外基本上都是常規迭代。但是細節方面的一些完善和提升對於浅羽來說還是比較受用的。可嘆的是 Lenovo 爲什麼非要給 WWAN 版專門單獨開模,簡直是一點升級空間都不想給用家留了。
浅羽把指點桿帽直接從 Gen 6 上取下裝到 Gen 9 上了,完全可以通用。
貼紙這一次沒有選擇 dbrand,而是找了據說是代工廠的一家店。店鋪內有包括 Swarm、Camouflage 在內的各種 dbrand 同款貼紙,同時也可以做 logo cutout。實際收到後,浅羽發現貼紙的材料應該是同款,但開模方面還是存在不小的公差,考慮到和 dbrand 的價格差距不算大,所以也很難說值得。
浅羽這次就要求做 ThinkPad 的 logo cutout,這樣還可以保留 A 面的狀態指示燈號。另一邊的 Lenovo logo 則是完全遮住,不過這代的 Lenovo logo 有一個下陷,浅羽乾脆就在中間貼了點奇怪的東西「自由之光」。
拆後蓋看了一下,RAM 焊死,没有空余插槽,甚至连 WLAN 模組都是焊死的,整机只有 SSD 可以替换。非 WWAN 機種,出廠沒有預置 WWAN 模組和天線;同時Lenovo 有單獨開模,機身上也沒有 SIM 卡槽開孔。不過拆開後蓋後可以看到 SIM 卡托盤還保留着,WWAN 接口也留空。
這代留給自行改裝和升級的空間已經基本沒有了,不過「可維護性」倒是依然很好,大部分組件靠螺絲(而不是膠水)固定──雖然也不知道有什麼可「維護」的就是了。
根據 TP 非官方情報站站長所說:
這是因為 X1 Carbon Gen9 的 WLAN Only 機種,整個 C Cover 都由鎂合金打造而成,即使加裝了 WWAN 天線,也想辦法塞入 SIM 卡了,WWAN 無線訊號卻會被鎂合金材質所屏蔽掉!至於 X1 Carbon Gen9 的 WWAN 機種,在 Palmrest 兩側前緣部位,也就是下圖中對應 WWAN 天線的部位,都改用玻璃纖維(GFRP)材質了,以利無線訊號收送。
ThinkPad X1 Carbon Gen9與X13 Gen2簡測心得(上) | TP非官方情報站
應該說,本來只需要付出購買 WWAN 模組和天線的成本,現在還要加上換掉 C 殼的高昂成本了。不過如果只是收訊較差的話,其實不改可能也「能用」。先前在 X1 Carbon Gen 7 上體驗過 LTE 連線,感覺非常用。有機會的話的話也會在 X1 Carbon Gen 9 上試試看,不過至少要等浅羽決定好買 4G 還是 5G 模組之後了。
(附頁爲筆電詳細規格)
]]>The post InfluxDB 2.4+grafana9.0部署 first appeared on Icebound.
]]>打开隐藏配置页面:http://192.168.1.1/hidden_version_switch.html,勾选 Telnet Enable,页面自动刷新即表示完成。如果页面打不开,可以试试 http://192.168.1.1/hidden_version_switch.gch。
开启 telnet 的目的是使得超级密码明文存储在文件中,方便我们获取。
使用 FTP 登录 192.168.1.1,账号密码均为 useradmin,/tmp/telnet_su_passwd 文件内容即为超级密码。
此时再回到光猫管理员登录页面,使用文件内的密码即可登录管理员账户。
]]>参考链接:https://www.right.com.cn/forum/thread-8249610-1-1.html
AirPlay 是 Apple 的屏幕镜像和投影协议,这个协议已经被逆向并且有软件实现了,例如 LetsView, AirServer。通过 AirPlay + 接收软件,我们可以将 iPad 画面镜像投影到电脑上,分辨率更高,而且没有色差,处理起来就更简单了。如果是安卓的平板,也可以通过 Chromecast 或者 Miracast 协议投影,原理是一样的。
当图像投影到 PC 上之后,就可以通过截取 PC 屏幕窗口的的方式获取到平板画面内容。第一种方案是使用 MSS,一个跨平台的 Python 截图包,它能以大约 20 FPS 的速度捕获图片。使用也很简单,首先需要获取投影软件的窗口位置和大小,然后按帧截图发送给 OpenCV 处理。实现的代码在这里:screen_capture.py。由于 MSS 并没有提供获取窗口大小的方法,它的区域捕获仅仅依靠的是屏幕坐标。所以获取窗口还是需要我们自己实现的,而这部分不是跨平台的,也没办法获知窗口移动。再加上 MSS 仅仅是截图,当窗口在后台时就失效了,使用起来并不方便。
而更好的方法是通过 OBS + 虚拟摄像头 + OpenCV,对的,就是平时游戏主播使用的直播软件。简单来说就是使用 OBS 捕获投影软件窗口,再通过虚拟摄像头输出给 OpenCV。OBS 软件本身是跨平台的(但是在不同平台可能会有些不同),FPS 要多少有多少,而且窗口可以被遮挡(窗口不能最小化到后台,但是可以放在另一个 Virtual Desktop),窗口移动什么的也完全没有问题,专业的确实就是专业。OBS 设置部分很简单,只要增加一个 Source,然后再根据需要调整输出分辨率就好了。Python 部分的源代码在:obs_capture.py。
Note:虽然最新的 OBS 自带了 Virtual Cam,但是似乎在 Windows 上和 OpenCV 有兼容性问题,捕获的画面是黑的,依旧需要使用插件解决。
现在采集到了平板上的游戏画面,下一步就是给画面进行分类,来获得游戏所处的界面。这么做主要有这些原因:
在这个项目中,我直接抄了 tensorflow 的 Image Classification Tutorial。对于这种标准的 UI 界面,随便什么模型效果应该都不差:classifier_training.ipynb。
做图片分类的第一步是采集训练样本,你会注意到 screen_capture.py 和 obs_capture.py 的 __main__
部分都有 cv.imwrite
以及对应的按键绑定的代码。我首先会在开启图像采集的过程中,游玩游戏,手动进行需要自动化的整个流程,手动或者每 1 秒地频率采集一些原始图像。然后对应每一个分类新建一个文件夹,例如在《猫之城》中,我有 fish_idle, fish_ring, fish_drag, fish_reward 和 not_supported 这样一些分类。然后将采集到的图片拖到对应分类的文件夹中,我对于分类和图片的选取是这样的:
然后就是套代码了,图片分类并不需要很高的图片分辨率,这里我随便选了一个 220x300 来保持图片宽高比,套示例模型就能达到 99% 的准确度了。因为是 UI 界面,也不存在裁切变换,之后实际测试结果也非常好。最后将分类列表和模型保存下来就可以啦。为了保存单个文件,并且减少体积,使用的是 TensorFlow Lite 模型,predict 的代码在 classifier.py。唯一需要注意的是使用的时候需要自己 resize 到 220x300,并且 OpenCV 图片的颜色是 BGR 而 tensorflow 是 RGB 的,需要要进行转换。其他就没什么了,总共有效代码也就 15 行,踩着巨人的肩膀,使用成熟的库之后还是挺简单的。
Note: 图片中的 fish_ring = 099%
就是图片分类的结果和 score,而其他的图片识别内容和辅助线就在下一篇 blog 中讲解啦。
解压安装包,得到 img 镜像文件
gzip -d friendlyarm_nanopi-r4s-squashfs-sysupgrade.img.gz
给 img 镜像文件末尾增加 6G 空白数据
dd if=/dev/zero bs=1G count=6 >> friendlyarm_nanopi-r4s-squashfs-sysupgrade.img
对 img 镜像文件进行分区调整,使分区占满整个镜像文件
parted friendlyarm_nanopi-r4s-squashfs-sysupgrade.img
# 显示出分区
print
# 将第二个分区调整为 100% 大小
resizepart 2 100%
# 完成退出
quit
最后,将 img 镜像文件打包成压缩文件
gzip friendlyarm_nanopi-r4s-squashfs-sysupgrade.img
这样最后得到的安装包就是扩容完成的了,用这个刷机即可,再也不会提示空间不足了。
]]>参考链接:https://dickies.myds.me:56789/st/routeos/1024/
The post [Leetcode系列]初级算法 first appeared on Icebound.
]]>言归正传,这次带来的是一个手游《猫之城》的物理挂。4年以前,我做过一个《少前》的脚本,使用的是一个机器内的 App 来采集图像然后驱动控制的,然后就被封号了:P 。于是这一次就打算使用物理的方法,在机器外部实现所有的信息处理和控制。比如下面这一段视频,就是这个物理挂识别游戏内的钓鱼小游戏,然后通过电极模拟触控实现的:
在这一系列 blog 中,我会分为
等篇章讲解这个 bot 的实现,同时源代码已经上传到了 github: cat-planet-bot。注意:使用外挂是违反游戏用户协议的行为。由于代码中使用了大量 hard coded 图像坐标,我并不认为你能直接使用它。这份代码仅在 blog 中作为引用,讲解学习。
我也是第一次做硬件,电子电路以及图像处理开发(所以使用的最简单的 Arduino),我只会很简单地介绍这方面的知识,如果有什么错误,或者更好的方案,欢迎在评论中指出。
简单的说,现在的平板手机的触摸屏都是电容屏,它是通过测量手指靠近屏幕导致的电容变化来获取点击位置的,所以只要你能造成屏幕上某个区域的电容变化,就能模拟出点击。不过,不管怎么说,不同屏幕实现方式和灵敏度还是有区别的,最靠谱简单的方式还是实践。毕竟只要某个方案在你自己的机器上有效就可以了嘛。这里我直接使用了一个网页,在 iPad 上测试了一些可行和不可行的例子:
不可行的:
可行的:
总结起来这里有两个关键:
于是,我这里选择的是:电击按摩贴。首先,这东西导电,而且可以随意裁剪大小,并且自带粘性,可以粘在屏幕上。通过控制是否接地(初次粘贴时屏幕会感应为持续按住状态,需要关闭再打开屏幕reset,原因见下文)可以控制触摸状态。最最最重要的是,这东西在美国很容易买到,并且我家里有:D 。如果你在国内,可以买到成品连点器,或者直接买吸盘造型的导电橡胶,价格实惠,卖家甚至已经给你接好了导线。
在上一步我们知道可以通过电极的接地与否,模拟触摸的按下和抬起,这时候就需要一个 PC 到这个接地电路的控制器,这里一般是一个与 PC 通信的 MUC 中导出的 GPIO 接口。我这里使用的是 Arduino,一个非常成熟的入门级开发板和配套程序。不过你也可以用例如 树莓派,STM32,ESP32 等等平台,它们的开发板可能更便宜,而且有的还能做到无线控制。
然后电路具体怎么实现呢?简单查询,网上有说使用继电器,伺服电机的,也有说可以使用 N-channel MOSFET(也有说用 P-channel 或者不能用的)。但是都有一个问题:没有实物,而且我也不知道需要买什么规格的啊。在美国,如果一次没有搞定,重复购买的话,光运费就会多花出很多钱了。于是,我把镜头看向了国内,刷到了 【单片机】Arduino光遇自动弹琴机器人2.0来了 这个视频,视频中 UP 主使用了光耦甚至给出了型号。那还说什么呢?照着来呗。
选择这个方案还有一些原因是,其他方案里的,伺服电机存在机械机构,安装麻烦;继电器往往是电磁继电器,开关时会发出噪音;而 MOSFET 的电路不完全隔离,可能会因为自身存在一些电容,而屏幕判断不准确。
在面包板上将一个 GPIO 端口,与一个发光二极管,光耦,限流电阻串联,然后接地就可以了;光耦的另一端分别接按摩贴做的电极和地:
在 GPIO 高电平时,光耦开关闭合,会将电极和地连通,从而模拟出按下的状态。
光耦和 LED 类似,需要串联一个限流电阻。例如我使用的 PC817 的 Forward Voltage 是 1.2V,电流 20mA。而 Arduino 的 GPIO 输出是 5V 的,通过 计算 我们需要串联一个大约 190Ω 的电阻。(什么,你说我还串联了一个 LED ?管它呢,又不是不能用)
Arduino 非常简单地提供了一个 IDE,插上开发板的 USB 就可以开始编程了,你首先可以照着 Arduino 的标准教程 Blink 熟悉下环境,当程序上传到开发板之后就不需要 IDE 了。PC 将通过 USB 连接 Arduino UART 串口进行通信。Arduino 部分的代码如下:
1 | void setup() { |
是不是很简单呢?这里首先初始化了开发板的串口,和一些 GPIO 作为输出。然后我们定义了一个通信协议:HIG00 和 LOW00 来控制 GPIO 的高低电平。注意,这里开发板在初始化后会立即发送一个 OK 信息,这是因为 Arduino UNO 会在串口连接上时重启,这个 OK 信息可以让 PC 端知道开发板已经准备好了。
PC 这边可以用 pySerial 这个包,当开发板连接上 PC 或者 Mac 之后,会显示为 COM* 或者 /dev/tty* 设备。pySerial 也有 serial.tools.list_ports.comports()
API 可以列出所有的串口设备,我们可以通过一些条件找到 Arduino:
https://github.com/binux/cat-planet-bot/blob/main/arduino.py
代码中我还实现了一些 helper function 例如 throttle_press
和 autorelease
并且记录了每个 pin 的按下状态。这些都只是为了方便,并不是必须的,串口的速率是完全足够直接发送每个指令的,并且我测试中也没有感觉到任何延迟。
只要在面包板上插上更多的光耦和 Arduino GPIO 连接,然后制作更多的电极,就能支持多点触控了。但是,很明显的,这个数量是有限制的。而且无法改变点击位置,也无法实现滑动。那么有解决方案吗?我这里有一些想法:
首先,可以增加电极的数量,以至于覆盖到整个屏幕,然后在屏幕上分区分块控制每个点的电容变化。这个想法在 这篇论文 中有描述,但是我并没有找到成品。
而另一个更可行的方案是通过机械控制触控笔,实现全屏覆盖。例如改装一个 3D 打印机,将喷头换成一只触控笔,通过 3D 打印机的精确 3 轴移动来模拟点击。这个方案的好处在于 3D 打印机是一个非常便宜的成品,省去了零散零件采购的成本,而且很多 3D 打印机的固件是开源的,可以很容易地通过 G-code 操作。
到这里,我们就打通了 PC 到平板的物理触控了。使用这些东西,就可以开发一些固定的自动化脚本了,例如下面这个脚本每次执行,自动按了 99 次 +10 然后购买:
在使用中,有一些影响触控成功率的经验:
本篇爲「家庭網路」系列第 15 篇(全 15 篇)。
網易的 UU 路由器插件支援 Merlin、小米路由器和 OpenWRT。但是如果沒有支援的路由器如何使用呢?開 VM 是最簡單的方式,先前也分享過一些技巧。容器作法也已經有現成的工作可以使用。不過在 Proxmox VE 上還可以使用 Linux 容器單獨運行 OpenWRT。
OpenWRT 現時已經提供了官方的 rootfs,直接下載就可以使用了。不過由於 Proxmox VE 上的 LXC 作業系統類型未有預設 OpenWRT,所以需要在命令列下建立容器:
export CTID=2000
pct create ${CTID} \
/path/to/storage/template/cache/openwrt-21.02-amd64.tar.xz \
--hostname openwrt-uu
--rootfs local-lvm:1 \
--cores 1 \
--memory 512 \
--arch amd64 \
--ostype unmanaged \
--unprivileged 0
pct set ${CTID} --net0 name=eth0,bridge=vmbr0,ip=manual
注意:由於容器後續需要使用 TUN 裝置,所以需要是特權容器(--unprivileged 0
)。至於其他部分則是看需求而定。
然後爲容器新增 TUN 裝置,編輯 /etc/pve/lxc/${CTID}.conf
加入以下行:
lxc.cgroup2.devices.allow: c 10:200 rwm
lxc.hook.autodev: sh -c "modprobe tun; cd ${LXC_ROOTFS_MOUNT}/dev; mkdir net; mknod net/tun c 10 200; chmod 0666 net/tun"
此時容器已經準備好,可以啓動了。
啓動容器後,OpenWRT 預設是從 DHCP 獲得 IP 位置的,並且預設的網路介面劃定爲 WAN 區域。由於 UU 加速器需要 br-lan
接口,爲了方便後續設定,編輯 /etc/config/network
以調整 OpenWRT 的網路設定:
config interface 'loopback'
option proto 'static'
option ipaddr '127.0.0.1'
option netmask '255.0.0.0'
option device 'lo'
option ifname 'lo'
config interface 'lan'
option type 'bridge'
option ifname 'eth0'
option proto 'static'
option ipaddr '{{UU_LAN_IPADDR}}'
option gateway '{{UU_LAN_GATEWAY}}'
option netmask '{{UU_LAN_NETMASK}}'
同時編輯 /etc/config/dhcp
關閉掉接口上的 DHCP:
config dhcp 'lan'
option interface 'lan'
option ignore '1'
option ra_management '0'
重新啓動容器,然後安裝 UU 路由器插件及第三方的 LuCI 管理介面:
opkg update
opkg install ca-certificates kmod-tun
opkg install uugamebooster luci-app-uugamebooster luci-i18n-uugamebooster-zh-cn
出於保險考慮,可以禁用掉防火牆之類的無關服務:
/etc/init.d/firewall disable
/etc/init.d/odhcpd disable
最後開啓 UU 加速器:
uci set uuplugin.uuplugin.enabled='1'
/etc/init.d/uuplugin enable
/etc/init.d/uuplugin start
在 DHCP 伺服器上,爲遊戲主機下發特定的設定檔,將閘道器和 DNS 設定為容器的 IP。同時,在手機上的「UU 主機加速」應用程式中綁定路由器插件前,需要將手機閘道器和 DNS 也設定爲容器的 IP;綁定完畢後,可以改回原來的設定。設定完成後,打開「UU 主機加速」應用程式即可看到裝置出現,正常操作加速即可。
第三方的 LuCI 管理介面儘管大家對於紅米 Redmi K40 系列褒貶不一,但其熱度無疑是足夠的。整個系列之中,定位最低的 K40 (在一系列優惠之後)是最受歡迎的。但是定位夾在中間的 K40 Pro 則在核心部分有諸多升級:
整體來看,除去 SoC 的升級外,其他方面可感知的改進不多。但考慮到 SD870 無法兩張 SIM 卡同時 5G 駐網,再加上 K40 在發表之後一時竟難以購買,浅羽最終還是選擇了「比較 Pro」的 K40 Pro。
浅羽是將 K40 Pro 作工具機用,主要解決電話、簡訊、地圖和支付功能。從這個角度出發,K40 Pro 應該說該有的功能都不缺,而且重量控制尚可;至於飽受詬病的塑料邊框,實際的質感也並不差,並且外觀上可以做到無斷點。K40 Pro 全系列都是使用了側邊指紋辨識,對於浅羽這種老索狗 Sony 老用家來說很是得討喜。側邊指紋辨識的好處之一是有準確的位置標識,並且兼顧靈敏與準確,實際體驗比 Xperia 的指紋辨識還是好不少。號稱「最小」的前鏡頭打孔確實很小,且周圍沒有額外一圈黑色區域;相比之下,小米 11 系列、小米 10S 和小米 Civi 反而都沒有做到。
當然,作爲一款「廉價」機種,K40 Pro 也有不少缺點──甚至可以說拿着好的硬體做着極差的體驗。首先是飽受詬病的 SD888 發熱問題,以 K40 Pro 上的散熱自然是無法完全發揮其效能。不過浅羽對於 K40 Pro 的定位就是電話支付機,極少使用它玩大型遊戲,倒也還算流暢。這當然同時也得益於宣傳的「三星 E4 AMOLED 硬性螢幕」的 120Hz 刷新率。不過,這片螢幕也僅有 1920×1080 的解析度,無硬體 DC 調光功能,久看易累不知是否於此有關;系統中也有原色顯示功能,但對於外部色溫的測定不準確,容易忽冷忽暖。
背面的一片霧面處理的玻璃算是外觀上的一大賣點,但是非常不耐刮擦,容易劃傷。浅羽剛拿到幾日就在擦拭灰塵時就不慎刮出幾道淺印,好在後來貼了保護貼後就看不出來了。而且有了好的先天條件卻沒有無線充電,有線充電也只給到 33W 的功率,應該是受限制於定位了。好在 NFC 和紅外都完整保留,這也算是小米(紅米)手機的保留項目了。
框內爲擦拭灰塵時不慎刮出的淺印最後在相機的方面。廣角加上超廣角的組合僅僅是保證能影到相,兩顆鏡頭都沒有 OIS,所以低光下基本也不用想什麼。另外,主鏡頭是 64MP(日常使用開啓 4-in-1 拍攝 12MP 相片),浅羽用得更多超廣角的畫素卻只給到 8MP。倒是附贈一顆 5MP 微距镜头頗爲有趣,用來拍攝細緻的貓咪皮膚紋理還是很好用的。浅羽其實對這部份不太在意,因爲影相還是用相機或者 instax 比較多,手機嘛可以記錄到就好了。
至於最終換掉它的理由也很樸素:強迫症浅羽不太喜歡螢幕上打孔……然後機緣巧合就換了屏下鏡頭的手機。閒置之後,浅羽開始思考給 K40 Pro 換個系統當作玩樂機,結果發現選擇寥寥無幾(截止 2022 年 2 月──是的,這篇網誌拖了半年);對比之下,去掉 Pro 的 K40 選擇則多了不少。這也從一個側面反應了不同機種的熱度差別。最終在新舊手機的過渡期結束時候,還是決定「手機不需要可以給有需要的人」,最終以首發價 45% off 轉讓了它。
最後看一眼(沒有本體的)全家福如果是 K40 代表的是極致的 C/P 值,對比之下 K40 Pro 就顯得有些尷尬了。背負了 Pro 之名的它既要做得更好、卻處處受限於品牌定位不能做到最好。這種「帶着鐐銬跳舞」的做法,不但考驗着廠牌如何做取捨,更考驗用家如何說服自己去接受一些不完美。也許想真正接受這樣的不完美,只有把付出的代價和期望都降低——比如,如果 K40 Pro 沒有那麼 Pro。
]]>Synology 暫未向 ARM 機種提供 Container Station,但浅羽需要使用一些第三方套件,因此只能透過 Entware 安裝。浅羽的 Entware 是在 DSM 6 時安裝的,更新至 DSM 7 後可以繼續使用,無需額外操作。更新完成後,iPKGui 等相關套件會提示損壞,但是無需理會;而且 opkg
是可以正常使用的。
由於 Easy Bootstrap Installer 需要使用 root 權限,所以暫時無法正常使用。不過仍然可以在 DSM 7 上手動安裝。
剛完成更新時,Surveilance Station 還停留在版本 8 上,套件中心也未能檢測到新版本,於是只好自己下載 SPK 包安裝。DSM 7 中似乎取消了 armada37xx
架構,安裝 armv8
的 SPK 包即可。
Surveillance Station 9 更新了 DSM 7 風格的用戶介面,並且整合了實時監看和回放功能,不過需要搭配 2.0 版本以上的 Surveillance Station Client。新版本的 Client 的用戶介面也變得更加現代了。但更新到 DSM 7.1 後,H.265 的相關授權轉移到 Advanced Media Extensions 內,並且要求登入 Synology 賬戶後才能安裝 HEVC codec pack。如果有需要使用 H.265 編碼但介意登入 Synology 的賬戶的話謹慎更新。
有部分套件未能隨 DSM 更新而更新,浅羽這邊就遇到 Cloud Sync 一例。這些套件在套件中心中顯示爲「已安裝」,但無法開啓。從套件中心中移除(選擇保留資料庫)、重新安裝以後,就可以正常開啓、使用了。
]]>第一步则是在 wp-content/themes
下创建子主题文件夹,为了方便,名称最好是在原主题文件夹名称后加上 -child
,以 Twenty Twelve 主题为例,子主题文件夹最好是 twentytwelve-child
,以下涉及到的文件都在子主题文件夹内。
接下来需要创建名为 style.css
的样式表文件,文件的开头需要有以下格式的内容,以便 WordPress 能正确识别这是一个子主题。
/*
Theme Name: Twenty Twelve Child
Description: Child theme for Twenty Twelve
Author: Neo
Author URI: https://www.whosneo.com/about/
Template: twentytwelve
Version: 0.1.0
*/
其中,有几项是必不可少的:
– Theme Name – 主题名称不可与其他主题相同
– Template – 父主题的文件夹名称
其余的信息可以选择性的添加。虽然子主题可以只有一个 style.css
样式表文件,但是不能缺了 functions.php
,否则便无法正确加载样式。
最后一步便是装载父主题和子主题的样式了。
以前通用的做法是在
style.css
文件中使用@import
引入父主题的样式,但现在已经不再推荐使用,因为这会增加样式加载时间,也有可能造成父主题样式重复加载。
装载主题推荐的方法就是在子主题的 functions.php
文件中增加一个 wp_enqueue_scripts
动作,并使用 wp_enqueue_style()
函数。如果不存在 functions.php
文件,则创建。同时不要忘了。所有的 php 代码都需要用 php 标签包围起来<?php 👻 ?>
。
<?php
add_action( 'wp_enqueue_scripts', 'enqueue_parent_styles' );
function enqueue_parent_styles() {
$theme = wp_get_theme();
wp_enqueue_style( 'parent-style', get_template_directory_uri() . '/style.css', array(), $theme->parent()->get('Version') );
wp_enqueue_style( 'child-style', get_stylesheet_uri(), array( 'parent-style' ), $theme->get('Version') );
}
?>
在后台管理 > 外观 > 主题 中,激活你的子主题即可。
除了上面提到的 style.css
functions.php
文件之外,添加的其他任何文件都会覆盖父主题的同名文件。例如你想修改父主题的页脚,就可以复制 footer.php
文件到子主题文件夹中进行修改。修改完成后,刷新页面就可以看到修改效果了。
近年來,越來越多的人購入了第二支手機,「備用機」這個概念也愈發流行。第二支手機用來「備用」的地方有很多,往往也是「專機專用」,區分用途。
最常見的用備用機的理由應該是「多張電話卡」了:在這個到處都要手機門號的年代,公開號碼、私人門號分開已經是再尋常不過的事情了;又或者一卡工作、一卡生活;再或者因爲計劃優惠,所以一卡通話、一卡流量。當電話卡越來越多,雙卡雙待不能滿足需求的時候,如果不想換「非洲之王」 TENCO 的四卡四待手機,只能多備一支手機了。多出的手機往往只用來接收簡訊或這電話驗證,。
隨着越來越多的屬性被附加在手機上,手機早已不是電話、簡訊能概括的了;手機之間不是相互取代的關係,而是而是漸漸地開始出現生態、區分「陣營」,被賦予更多「定位」和「屬性」,各有所長、又各有所短。往往是「大螢幕,我所欲也;單手操作,亦我所欲也」、「拍照,我所欲也;輕薄,亦我所欲也」;這種兩支手機分工合作,既佔盡了一支的優勢、又可以另一支形成互補。反而頗有一種「分久必合,合久必分」的意味了。舉例來說:一機打機、聽音樂、看影片,主打娛樂,電量隨心用;一機通話、訊息、支付,主打生活,保證全天候──這樣的「雙機黨」也是愈發常見了。近年來,手機換代很快,往往買了新款,舊的還用得好好的;如此正好以舊手機應付生活通訊,不用來回遷移和同步資料,而以新手機之最新硬體應付娛樂,可謂是兩全其美。同時,不同廠牌的手機體驗也不盡相同,兩支手機也可以作爲體驗之用、或作爲更換廠牌的過渡。更爲極端一些,有些手機遊戲會區分 iOS 和 Android 平臺的帳號,不同平臺之間帳號無法通用,也就不難理解許多用家在更換手機後依然保留另一平臺的裝置用作娛樂的現象了。也有人選擇儘量用平板解決一些需求,只把手機作爲通話和數據資料之用。這樣一直不更換的手機就是「主力機」,而時常更換的、較新的裝置反而成爲了「備用」。
浅羽自己也在使用多台裝置。浅羽的主要手機是 Hisense A5 Pro,它主打一塊電子紙螢幕,在戶外有絕佳的可讀性。對於浅羽來說,不出門時則將它當作傻瓜機(dumb phone)使用,大部分時候無需理會它;而出門在外時,它可以滿足最基礎的聯絡、地圖和支付需求,並且充電一次可以使用 3-4 天,無需特別計劃出門時間然後提前充好電。唯其更新率偏低、也不支援彩色顯示,因此使用其他裝置進行補充也是在所難免的。類似這樣傻瓜機爲主、其他裝置作爲補充的組合也非常常見,無論是不想頻繁充電、不想被太多訊息干擾或是踐行數位極簡主義的用家都不少。這種狀況下,其實到底哪隻手機是作爲「主力」、哪隻作爲「備用」都很難界定了。
最後,還聽說有人使用第二支手機的原因是工作要求:比如自己使用 iPhone,但工作運用到只能在 Android 上運行應用程式;比如自己使用其他廠牌,但是工作僱主對手機廠牌有強硬要求……這種爲了滿足某些要求而添置的手機,反倒更接近真正意義上的「備用」了──因爲不到非用不可的場合,可能不會真的去使用吧。
]]>The post 自动驾驶中的名词解释 first appeared on Icebound.
]]>本文假设注册了域名 example.com,并给路由器分配一个域名 openwrt1.example.com,且已经设置好该域名的 AAAA 记录为路由器的公网 IPv6 地址。
在 系统 – 软件 中搜索安装 luci-i18n-ddns-zh-cn ddns-scripts-cloudflare bind-host ca-certificates
,系统会自动安装好所有的依赖。
在 服务 – 动态 DNS 中,点击左下角添加新服务,名称可以随意填写,IP 地址版本这里选择 IPv6,DDNS 服务提供商这里本文以 Cloudflare 为例所以选择 cloudflare-v4:
在 Cloudflare 的 API 中,用户名对应 Cloudflare 账户的登录邮箱,密码对应的则是 API Key。
登录 Cloudflare 账户之后,访问 API 令牌页面 https://dash.cloudflare.com/profile/api-tokens 即可在下方看到 API 密钥,在下方的 Global API Key 的右侧点击查看,输入 Cloudflare 账户的密码之后即可获取 API Key。
获取到 API Key 之后就可以把基本设置中的条目填写完成了:
高级设置中,需要将 IP 地址来源修改为接口,接口这里由于是路由器拨号上网,所以我选择了 pppoe-wan,其余选项可以不用调整也可以照图里的设置来:
计时器设定中,检查间隔可以根据自己的需要来,间隔小的话更新 IP 地址就更快速:
保存并应用所有设置之后,点击新增配置的重新加载,即可启动 DDNS 更新服务:
首先创建 Swap 文件:
sudo fallocate -l 4G /swapfile
如果 fallocate
不可用,则可以换用另一种方法:
sudo dd if=/dev/zero of=/swapfile bs=1024 count=4194304
设置权限,确保只有 root 用户可以读写 Swap 文件:
sudo chmod 600 /swapfile
接下来,在 Swap 文件上设置 Swap 分区:
sudo mkswap /swapfile
然后就是激活 Swap 分区:
sudo swapon /swapfile
激活之后可以使用 free -h
命令查看 Swap 分区的使用情况,以验证是否成功激活 Swap 分区。
最后,需要在 /etc/fstab
文件的最后添加条目,确保设置永久生效:
/swapfile swap swap defaults 0 0
Swappiness 值是 Linux 内核中定义的使用 Swap 分区的优先级,值可以为 0 至 100 之间的任何整数。值越小系统就尽量少去使用 Swap 分区,值越大系统就尽可能多的去使用 Swap 分区。
在 CentOS 上,Swappiness 值默认是 30。可以通过以下命令检查当前系统的 Swappiness 值:
cat /proc/sys/vm/swappiness
30 的 Swappiness 值对于桌面系统来说是合适的,但对于服务器来说,小一点可能会合适。例如,设置 Swappiness 值为 10:
sudo sysctl vm.swappiness=10
如果想永久设置 Swappiness 值,需要编辑 /etc/sysctl.conf
文件,在末尾添加以下内容:
vm.swappiness=10
首先,取消激活 Swap 分区:
sudo swapoff -v /swapfile
然后,将 /etc/fstab
文件中的条目 /swapfile swap swap defaults 0 0
删除。
最后,删除 Swap 文件:
sudo rm /swapfile
]]>在两台路由器上均进行安装。
opkg update
opkg install tinc
此处假设使用两台路由器来配置互相连接,名称为 A 和 B。
路由器 A 的 DDNS 地址为 A.router.com,路由器 B 的 DDNS 地址为 B.router.com,这里解析出来的地址无论是 IPv4 还是 IPv6 都没关系,只要是公网能访问的地址就可以。
路由器 A 和 B 均使用 665 端口监听 tinc 的传入连接。
路由器 A 的局域网为 192.168.88.0/24,路由器 B 的局域网为 192.168.99.0/24。
在 tinc 网络中,路由器 A 使用 IP 10.0.0.88,路由器 B 使用 IP 10.0.0.99。
首先需要定义一个网络名称,这里使用 tincnet
。
在 /etc/tinc/
目录中新建文件夹,名字为网络名称,即创建 /etc/tinc/tincnet/
目录。
在此目录下创建 tinc.conf tinc-up tinc-down
文件和 hosts
文件夹。
tinc.conf
为 tincnet
的配置文件,tinc-up
为启动该网络时自动执行的脚本,tinc-down
为关闭该网络时自动执行的脚本,hosts
文件夹保存着各个节点(路由器)的信息。
tinc.conf
文件中保存着路由器 A 的配置信息:
Name = A #路由器 A 的名称
BindToAddress = * 665 #监听端口
Interface = tinc #虚拟接口名称
Device = /dev/net/tun
Mode = switch #交换机模式
ConnectTo = B #默认连接路由器 B
tinc-up
文件是网络启动时执行的脚本,一般在这里为 tinc 的接口添加地址信息:
#!/bin/sh
ip link set $INTERFACE up
ip addr add 10.0.0.88/24 dev $INTERFACE
ip route add 192.168.99.0/24 via 10.0.0.99 dev tinc src 10.0.0.88 onlink
tinc-down
文件是网络关闭时执行的脚本,一般在这里为 tinc 的接口清除配置:
#!/bin/sh
ip route del 192.168.99.0/24
ip addr del 10.0.0.88/24 dev $INTERFACE
ip link set $INTERFACE down
配置好 tinc-up tinc-down
文件之后,不要忘了为这两个文件增加执行权限:
chmod +x tinc-up
chmod +x tinc-down
hosts
文件夹中先创建好本机的文件,名称需要与上方 tinc.conf
中第一行的配置保持一致,即文件 A
:
Address = A.router.com
Port = 665
Subnet = 10.0.0.88/32
接下来为 tincnet
创建密钥信息,在询问保存位置时直接回车使用默认位置即可:
tinc -n tincnet generate-rsa-keys
tinc -n tincnet generate-ed25519-keys
此时路由器 A 的 /etc/tinc/tincnet/
目录下的情况为:
├── ed25519_key.priv
├── hosts/
│ └── A
├── rsa_key.priv
├── tinc-down*
├── tinc-up*
└── tinc.conf
最后需要编辑 /etc/config/tinc
文件:
config tinc-net tincnet
option enabled 1
option Name A
config tinc-host A
option enabled 1
option net tincnet
首先需要定义一个网络名称,这里还是使用 tincnet
。
在 /etc/tinc/
目录中新建文件夹,名字为网络名称,即创建 /etc/tinc/tincnet/
目录。
在此目录下创建 tinc.conf tinc-up tinc-down
文件和 hosts
文件夹。
tinc.conf
为 tincnet
的配置文件,tinc-up
为启动该网络时自动执行的脚本,tinc-down
为关闭该网络时自动执行的脚本,hosts
文件夹保存着各个节点(路由器)的信息。
tinc.conf
文件中保存着路由器 B 的配置信息:
Name = B #路由器 B 的名称
BindToAddress = * 665 #监听端口
Interface = tinc #虚拟接口名称
Device = /dev/net/tun
Mode = switch #交换机模式
ConnectTo = A #默认连接路由器 A
tinc-up
文件是网络启动时执行的脚本,一般在这里为 tinc 的接口添加地址信息:
#!/bin/sh
ip link set $INTERFACE up
ip addr add 10.0.0.99/24 dev $INTERFACE
ip route add 192.168.88.0/24 via 10.0.0.88 dev tinc src 10.0.0.99 onlink
tinc-down
文件是网络关闭时执行的脚本,一般在这里为 tinc 的接口清除配置:
#!/bin/sh
ip route del 192.168.88.0/24
ip addr del 10.0.0.99/24 dev $INTERFACE
ip link set $INTERFACE down
配置好 tinc-up tinc-down
文件之后,不要忘了为这两个文件增加执行权限:
chmod +x tinc-up
chmod +x tinc-down
hosts
文件夹中先创建好本机的文件,名称需要与上方 tinc.conf
中第一行的配置保持一致,即文件 B
:
Address = B.router.com
Port = 665
Subnet = 10.0.0.99/32
接下来为 tincnet
创建密钥信息,在询问保存位置时直接回车使用默认位置即可:
tinc -n tincnet generate-rsa-keys
tinc -n tincnet generate-ed25519-keys
此时路由器 B 的 /etc/tinc/tincnet/
目录下的情况为:
├── ed25519_key.priv
├── hosts/
│ └── B
├── rsa_key.priv
├── tinc-down*
├── tinc-up*
└── tinc.conf
最后需要编辑 /etc/config/tinc
文件:
config tinc-net tincnet
option enabled 1
option Name B
config tinc-host B
option enabled 1
option net tincnet
需要将路由器 A 上的 /etc/tinc/tincnet/hosts/A
文件复制到路由器 B 的/etc/tinc/tincnet/hosts/
目录下,同理,需要将路由器 B 上的 /etc/tinc/tincnet/hosts/B
文件复制到路由器 A 的/etc/tinc/tincnet/hosts/
目录下。
此时两个路由器的 /etc/tinc/tincnet/
目录下均为这样:
├── ed25519_key.priv
├── hosts/
│ ├── A
│ └── B
├── rsa_key.priv
├── tinc-down*
├── tinc-up*
└── tinc.conf
需要注意两个路由器要在防火墙中开放 665 端口。
在两个路由器上均启动 tinc,即可连通两个路由器:
/etc/init.d/tinc start
如果需要更省心的操作,可以参考上一篇文章中的自动化的配置。
]]>自從京東「618 店慶」、淘寶「雙 11」開了購物節的先河後,「購物節」越辦越多,而且每次都有「預熱」、「返場」,時間越來越長,可以說是「每月一次節,每次一個月」。經過幾年的大力宣傳炒作,購物節的形式也漸漸被大家所認識和瞭解;浅羽也不能免俗,想要薅一把羊毛。
平臺促銷,什麼「秒殺」「限時優惠」已經是司空見慣了,守着螢幕等着零點時分也是家常便飯,不過近年的促銷活動週期拉得更長,也就少了些立減限量搶購。優惠的大頭還是滿額減價;有時爲了享受到優惠就不免要湊單。極端一點如浅羽本貓甚至變成了從按需消費變成了計劃消費,購買品牌產品漸漸地就變成了「等 618」「等雙 11」。除掉急用的必需品,其餘的儘量都在「購物節」時購買,甚至於日用品也選擇在優惠期間大量購買「屯貨」,徹底過成了優惠日曆的樣子。
有些情況則是比較可氣,比如說店家要參加「滿 200 減 30」的活動,原本標價 200 元出個頭的產品,在活動期間內突然做小幅度的減價,以至於不能輕易湊夠滿減。可是眼見稍加一點就可以夠上滿減的門檻,怎麼辦呢?這種時候只好去尋找「湊單品」。久而久之,訂單記錄裏就堆滿了各種幾塊錢的小商品,家裏也堆滿了沒什麼用的小零碎。好在這樣一通操作下來,原本打算購買的商品還是獲得了一定的優惠。也有甚者,爲了不多花一分錢,用隨機商品湊單之後再申請退款,只能說與活動的策劃者也不是無辜的。
希望有朝一日能看到這些平臺有更純粹的促銷活動,而非不停地佈置的「消費主義陷阱」;不需要分享「砍價」、不需要計算湊單,更不用爲了尷尬的紅包每天花費時間在毫無樂趣的「遊戲」上。
]]>在两台路由器上均安装相关的依赖,只需要安装 luci-i18n-wireguard-zh-cn
系统即可自动将所有依赖安装完毕,完成之后必须要重启路由器。
opkg update
opkg install luci-i18n-wireguard-zh-cn
重新登录路由器管理页面,就可以在 状态 – WireGuard 中看到 WireGuard 连接的状态。
若使用 N 台路由器互相连接,则需要在每台路由器上设置 N-1 个对端,这样可以确保任意两台路由器之间能互相连接。此处假设使用两台路由器来配置互相连接,每台路由器上均需要设置一个对端。
路由器 A 的网段为 192.168.1.0/24,路由器 B 的网段为 192.168.2.0/24,我们选择为 WireGuard 网络设置网段 172.16.0.0/16,路由器 A 在 WireGuard 网络中的地址为 172.16.1.1,路由器 B 在 WireGuard 网络中的地址为 172.16.2.1。
进入 网络 – 接口 页面中,添加新的接口,接口名称填入 wg,协议需要选择 WireGuard VPN。
在 常规设置 中,点击生成密钥按钮,私钥栏中会自动生成内容,监听端口填写 12000,IP 地址填入 172.16.1.1/16,注意此处最好是带上网段。
在 高级设置 中,MTU 填入 1280,此处如果填大了会让两端连接速度变慢。
在 防火墙设置 中,新建一个 vpn 区域。
保存并应用,创建端口。
在 状态 – WireGuard 中可以看到路由器 A 的公钥,复制下来,等下要填入路由器 B 的对端设置中。
回到 网络 – 接口 中,编辑 wg 接口,在 对端 中,添加一个对端,描述可以写 OpenWrt2,公钥中填入路由器 B 的公钥(参考本文后面路由器 B 的设置获得公钥),允许的 IP 中填入 172.16.2.1/32 和 192.168.2.0/24,勾选路由允许的 IP,端点主机填入对端路由器的公网地址,端点端口填入对端路由器的端口 12000,持续 Keep-Alive 中可以填入自己喜欢的大小,如 15,保存并应用所有设置。
进入 网络 – 防火墙 – 常规设置 – 区域 中,编辑 lan 区域,将允许转发到目标区域勾选上 vpn 区域,将允许来自源区域的转发也勾选上 vpn 区域,保存;将 vpn 区域的转发改为接受,保存并应用设置。
在 网络 – 防火墙 – 通信规则 页面中,新增一项规则。
在 常规设置 中,名称填写 Allow-WireGuard,协议保持默认 TCP+UDP 即可,源区域选择 wan,目标区域选择 设备(输入),目标端口调入 WireGuard 监听端口 12000,操作选择接受,保存防火墙设置。
回到 网络 – 接口 页面中,重启 wg 接口。
进入 网络 – 接口 页面中,添加新的接口,接口名称填入 wg,协议需要选择 WireGuard VPN。
在 常规设置 中,点击生成密钥按钮,私钥栏中会自动生成内容,监听端口填写 12000,IP 地址填入 172.16.2.1/16,注意此处最好是带上网段。
在 高级设置 中,MTU 填入 1280,此处如果填大了会让两端连接速度变慢。
在 防火墙设置 中,新建一个 vpn 区域。
保存并应用,创建端口。
在 状态 – WireGuard 中可以看到路由器 B 的公钥,复制下来,等下要填入路由器 A 的对端设置中。
回到 网络 – 接口 中,编辑 wg 接口,在 对端 中,添加一个对端,描述可以写 OpenWrt1,公钥中填入路由器 A 的公钥(参考本文前面路由器 A 的设置获得公钥),允许的 IP 中填入 172.16.1.1/32 和 192.168.1.0/24,勾选路由允许的 IP,端点主机填入对端路由器的公网地址,端点端口填入对端路由器的端口 12000,持续 Keep-Alive 中可以填入自己喜欢的大小,如 15,保存并应用所有设置。
进入 网络 – 防火墙 – 常规设置 – 区域 中,编辑 lan 区域,将允许转发到目标区域勾选上 vpn 区域,将允许来自源区域的转发也勾选上 vpn 区域,保存;将 vpn 区域的转发改为接受,保存并应用设置。
在 网络 – 防火墙 – 通信规则 页面中,新增一项规则。
在 常规设置 中,名称填写 Allow-WireGuard,协议保持默认 TCP+UDP 即可,源区域选择 wan,目标区域选择 设备(输入),目标端口调入 WireGuard 监听端口 12000,操作选择接受,保存防火墙设置。
回到 网络 – 接口 页面中,重启 wg 接口。
此时,稍等片刻,在两台路由器的 状态 – WireGuard 中即可看到另外一台路由器的连接信息。
此时两端路由器下的设备就都可以访问另一端路由器下的设备了,就像是在同一个局域网中。
如果需要更省心的操作,可以选择参考以下内容。
当其中一个路由器重新连接网络之后,公网地址可能会发生变化,此时可能会无法连接 WireGuard,不过我们可以配合 DDNS 使用。
两台路由器均配置好 DDNS 之后,可以在 WireGuard 的对端配置中将端点主机的地址由 IP 地址更换为域名。
DDNS 配置好之后,这样即使路由器更换了 IP 地址也可以重新连接上。
WireGuard 可完美兼容 IPv6 使用,只需要 DDNS 的域名只有 IPv6 地址记录即可,在这种情况下,WireGuard 就使用 IPv6 与其他路由器连接。只需将上文图中 172.27.0.0/16 的地址替换为 DDNS 域名即可。
IPv6 DDNS 的配置可以参考另一篇文章:OpenWrt IPv6 DDNS
当出现问题无法连通对端路由器的时候,如果有自动化的操作可以直接重启接口,就很方便了,省去手动重启操作。
opkg update
opkg install watchcat luci-app-watchcat luci-i18n-watchcat-zh-cn
在 服务 – WatchCat 页面中,增加一项配置,模式选择“重启接口”(有些固件可能会翻译为“重启实例”),周期可以设置为 1m,在路由器 A 上安装的 WatchCat 就填写路由器 B 的地址 192.168.2.1,在路由器 B 上安装的 WatchCat 就填写路由器 A 的地址 192.168.1.1,检查间隔填写 10s,接口的话就选择 wg,其余保持不动,保存应用所有设置。
这样,在路由器发现无法 ping 通对端路由器 1 分钟之后将会自动重启 wg 接口,保证两台路由器之间持续连接。
]]>本文假设两台路由器分别叫 OpenWrt1 和 OpenWrt2,其中 OpenWrt1 作为主路由,OpenWrt2 作为旁路由。OpenWrt1 的局域网为 192.168.1.0/24,OpenWrt2 的局域网为 192.168.2.0/24。
首先去 https://my.zerotier.com 注册账户,然后在 https://my.zerotier.com/network 网络页面,点击页面上方一个大大的黄色按钮“Create A Network”即可创建完成一个网络,在下方的列表中,获得你的网络 ID。
两台路由器均安装 Zerotier:
opkg install zerotier
两台路由器均编辑 Zerotier 配置:
vi /etc/config/zerotier
修改里面的内容,将 ‘xxxxxxxxxxxxxxxx’ 修改为自己的 Zerotier 网络 ID。
config zerotier 'sample_config'
option enabled '1'
list join 'xxxxxxxxxxxxxxxx'
option nat '1'
option secret ''
两台路由器均启动 Zerotier:
/etc/init.d/zerotier start
网络 – 防火墙 – 常规设置 – 区域设置,将“转发”修改为“接受”:
在两台路由器都启动了 Zerotier 之后,在 Zerotier 的网络管理 Members 里面就可以看到两个设备了,在前面的框内都打上勾,稍等一下就可以在右边看到两个 IP 地址了:
再到两台路由器上运行一下 Zerotier 的命令来检查一下两台路由器到底分配的是哪个 IP:
zerotier-cli listnetworks
从命令输出的最后可以出来当前路由器使用的是哪个 IP。
本文中 OpenWrt1 为 10.147.19.32,OpenWrt2 为 10.147.19.12。
然后在 Zerotier 网络管理的高级设置中,添加对应的路由表,Destination 填入路由器的网段,Via 填入其得到的 Zerotier 地址:
在这番设置之后,便可以在不同网段之间互相访问其他局域网中的设备了。
网上充斥着大量的教程,却也没有说清楚旁路由 Zerotier 到底应该如何配置。在经历了两天的配置之后,终于将主路由/旁路由任意搭配使用的配置总结好。记录一下,以便查阅。
]]>本篇爲「家庭網路」系列第 14 篇(全 15 篇)。
網易 UU 加速器不知道爲何被戲稱爲「富家子弟加速器」。不過傻老婆恰好還有一段時間的訂閱,而且可以安裝在網關上,給遊戲主機加速,所以也不妨用用看。
網易 UU 加速器有官方的路由器插件,支援 OpenWRT 系統,但需要登入 SSH 手動執行指令稿安裝。好在有人類打包了 luci-app-uugamebooster
,可以直接安裝而不用執行內容繁多的指令稿。不過還是要保證 kmod-tun
已經安裝並正確工作。
加速器的路由器插件實際上是不停更新的,有時候版本太舊是會導致綁定失敗。從官方下載最新的插件包:
curl -s "http://router.uu.163.com/api/plugin?type=openwrt-x86_64" | jq .url | sed -E 's|/([^/"]+)|/{\1}|g' | xargs curl -o "#2-#3-#4.tar.gz"
解壓縮以後將所有的檔案都拷貝到 OpenWRT 上的 /usr/share/uugamebooster
中並覆蓋所有。
一切準備就緒,浅羽嘗試使用客戶端應用程式綁定路由器,但卻一直收到「該路由器型號暫不支持加速」的錯誤。可是路由器插件已經在正常運行了。正常的想法是打開調試開關檢查日誌,可是這個 uuplugin
怎麼都不說話,只好拆可執行檔看看:
uVar7 = 0;
if (*(long *)(unaff_RBP + -0x12d70) != 0) {
uVar7 = *(undefined8 *)(unaff_RBP + -0x12d78);
}
/* try { // try from 00401476 to 0040159d has its CatchHandler @ 00401a39 */
DAT_00903308 = FUN_0041cb77(uVar7,"br-lan");
再對比一下安裝時候用的指令稿生成 S/N 的相關函式:
print_sn() {
local interface=""
case "${ROUTER}" in
${ASUSWRT_MERLIN})
interface="br0"
;;
${XIAOMI} | ${HIWIFI} | ${OPENWRT})
interface="br-lan"
;;
*)
return 1
;;
esac
local info=$(ip addr show ${interface})
local mac=$(echo "${info}" | grep "link/ether" | awk '{print $2}')
echo "sn=${mac}"
return 0
}
可以注意到兩邊都會使用 br-lan
接口的 MAC 地址計算出一個 S/N 類似物。巧的是浅羽的 LAN 口是獨立的接口,沒有 br-lan
,大約因此路由器插件無法正確生成 S/N,導致客戶端綁定失敗。解決方法也很簡單,在「接口」頁面上修改 LAN 端口,從「物理設定」中找到「橋接接口」勾選保存即可。等網路恢復後,在客戶端中就可以正常綁定路由器並開啓加速了。
針對路由器不是 OpenWRT 或者無法安裝網易 UU 加速器路由器插件的情況,還可以選擇其他部署方式:
透過查看安裝的指令稿內容,可以發現其實官方準備了卸載指令稿。卸載指令稿的檔案會在安裝時自動下載,並放置到安裝指令稿所在的資料夾內。不過如果找不到也沒關係,可以自己下載然後執行。
UU_UNINST_DOWNLOAD_URL="router.uu.163.com/api/script/uninstall?type=openwrt"
UU_UNINST="/tmp/uu_uninst.sh"
wget -O ${UU_UNINST} ${UNINSTALL_DOWNLOAD_URL}
chmod +x ${UU_UNINST}
/bin/sh ${UU_UNINST} openwrt
]]>本文撰写于 Vite-0.9.1 版本。
借用作者的原话:
Vite,一个基于浏览器原生 ES imports 的开发服务器。利用浏览器去解析 imports,在服务器端按需编译返回,完全跳过了打包这个概念,服务器随起随用。同时不仅有 Vue 文件支持,还搞定了热更新,而且热更新的速度不会随着模块增多而变慢。针对生产环境则可以把同一份代码用 rollup 打包。虽然现在还比较粗糙,但这个方向我觉得是有潜力的,做得好可以彻底解决改一行代码等半天热更新的问题。
注意到两个点:
因此,要实现上述目标,需要要求项目里只使用原生 ES imports,如果使用了 require 将失效,所以要用它完全替代掉 Webpack 就目前来说还是不太现实的。上面也说了,生产模式下的打包不是 Vite 自身提供的,因此生产模式下如果你想要用 Webpack 打包也依然是可以的。从这个角度来说,Vite 可能更像是替代了 webpack-dev-server 的一个东西。
Vite 的实现离不开现代浏览器原生支持的 模块功能。如下:
1 | <script type="module"> |
当声明一个 script
标签类型为 module
时,浏览器将对其内部的 import
引用发起 HTTP
请求获取模块内容。比如上述,浏览器将发起一个对 HOST/a.js
的 HTTP 请求,获取到内容之后再执行。
Vite 劫持了这些请求,并在后端进行相应的处理(比如将 Vue 文件拆分成 template
、style
、script
三个部分),然后再返回给浏览器。
由于浏览器只会对用到的模块发起 HTTP 请求,所以 Vite 没必要对项目里所有的文件先打包后返回,而是只编译浏览器发起 HTTP 请求的模块即可。这里是不是有点按需加载的味道?
看到这里,可能有些朋友不免有些疑问,编译和打包有什么区别?为什么 Vite 号称「热更新的速度不会随着模块增多而变慢」?
简单举个例子,有三个文件 a.js
、b.js
、c.js
1 | // a.js |
1 | // c.js |
如果以 c 文件为入口,那么打包就会变成如下(结果进行了简化处理):(假定打包文件名为 bundle.js
)
1 | // bundle.js |
值得注意的是,打包也需要有编译的步骤。
Webpack 的热更新原理简单来说就是,一旦发生某个依赖(比如上面的 a.js
)改变,就将这个依赖所处的 module
的更新,并将新的 module
发送给浏览器重新执行。由于我们只打了一个 bundle.js
,所以热更新的话也会重新打这个 bundle.js
。试想如果依赖越来越多,就算只修改一个文件,理论上热更新的速度也会越来越慢。
而如果是像 Vite 这种只编译不打包会是什么情况呢?
只是编译的话,最终产出的依然是 a.js
、b.js
、c.js
三个文件,只有编译耗时。由于入口是 c.js
,浏览器解析到 import { a } from './a'
时,会发起 HTTP 请求 a.js
(b 同理),就算不用打包,也可以加载到所需要的代码,因此省去了合并代码的时间。
在热更新的时候,如果 a
发生了改变,只需要更新 a
以及用到 a
的 c
。由于 b
没有发生改变,所以 Vite 无需重新编译 b
,可以从缓存中直接拿编译的结果。这样一来,修改一个文件 a
,只会重新编译这个文件 a
以及浏览器当前用到这个文件 a
的文件,而其余文件都无需重新编译。所以理论上热更新的速度不会随着文件增加而变慢。
当然这样做有没有不好的地方?有,初始化的时候如果浏览器请求的模块过多,也会带来初始化的性能问题。不过如果你能遇到初始化过慢的这个问题,相信热更新的速度会弥补很多。当然我相信以后尤大也会解决这个问题。
上面说了这么多的铺垫,可能还不够直观,我们可以先跑一个 Vite 项目来实际看看。
按照官网的说明,可以输入如下命令(<project-name>
为自己想要的目录名即可)
1 | $ npx create-vite-app <project-name> |
如果一切都正常你将在 localhost:3000
(Vite 的服务器起的端口) 看到这个界面:
并得到如下的代码结构:
1 | . |
接下来开始说一下 Vite 实现的核心——拦截浏览器对模块的请求并返回处理后的结果。
我们知道,由于是在 localhost:3000
打开的网页,所以浏览器发起的第一个请求自然是请求 localhost:3000/
,这个请求发送到 Vite 后端之后经过静态资源服务器的处理,会进而请求到 /index.html
,此时 Vite 就开始对这个请求做拦截和处理了。
首先,index.html
里的源码是这样的:
1 | <div id="app"></div> |
但是在浏览器里它是这样的:
注意到什么不同了吗?是的, import { createApp } from 'vue'
换成了 import { createApp } from '/@modules/vue
。
这里就不得不说浏览器对 import
的模块发起请求时的一些局限了,平时我们写代码,如果不是引用相对路径的模块,而是引用 node_modules
的模块,都是直接 import xxx from 'xxx'
,由 Webpack 等工具来帮我们找这个模块的具体路径。但是浏览器不知道你项目里有 node_modules
,它只能通过相对路径去寻找模块。
因此 Vite 在拦截的请求里,对直接引用 node_modules
的模块都做了路径的替换,换成了 /@modules/
并返回回去。而后浏览器收到后,会发起对 /@modules/xxx
的请求,然后被 Vite 再次拦截,并由 Vite 内部去访问真正的模块,并将得到的内容再次做同样的处理后,返回给浏览器。
上面说的这步替换来自 src/node/serverPluginModuleRewrite.ts
:
1 | // 只取关键代码: |
如果并没有在 script
标签内部直接写 import
,而是用 src
的形式引用的话如下:
1 | <script type="module" src="/main.js"></script> |
那么就会在浏览器发起对 main.js
请求的时候进行处理:
1 | // 只取关键代码: |
替换逻辑 rewriteImports
就不展开了,用的是 es-module-lexer
来进行的语法分析获取 imports
数组,然后再做的替换。
如果 import
的是 .vue
文件,将会做更进一步的替换:
原本的 App.vue
文件长这样:
1 | <template> |
替换后长这样:
1 | // localhost:3000/App.vue |
这样就把原本一个 .vue
的文件拆成了三个请求(分别对应 script
、style
和template
) ,浏览器会先收到包含 script
逻辑的 App.vue
的响应,然后解析到 template
和 style
的路径后,会再次发起 HTTP 请求来请求对应的资源,此时 Vite 对其拦截并再次处理后返回相应的内容。
如下:
不得不说这个思路是非常巧妙的。
这一步的拆分来自 src/node/serverPluginVue.ts
,核心逻辑是根据 URL 的 query 参数来做不同的处理(简化分析如下):
1 | // 如果没有 query 的 type,比如直接请求的 /App.vue |
上面只涉及到了替换的逻辑,解析的逻辑来自 src/node/serverPluginModuleResolve.ts
。这一步就相对简单了,核心逻辑就是去 node_modules
里找有没有对应的模块,有的话就返回,没有的话就报 404:(省略了很多逻辑,比如对 web_modules
的处理、缓存的处理等)
1 | // ... |
上面已经说完了 Vite 是如何运行一个 Web 应用的,包括如何拦截请求、替换内容、返回处理后的结果。接下来说一下 Vite 热更新的实现,同样实现的非常巧妙。
我们知道,如果要实现热更新,那么就需要浏览器和服务器建立某种通信机制,这样浏览器才能收到通知进行热更新。Vite 的是通过 WebSocket
来实现的热更新通信。
客户端的代码在 src/client/client.ts
,主要是创建 WebSocket
客户端,监听来自服务端的 HMR 消息推送。
Vite 的 WS 客户端目前监听这几种消息:
connected
: WebSocket 连接成功vue-reload
: Vue 组件重新加载(当你修改了 script 里的内容时)vue-rerender
: Vue 组件重新渲染(当你修改了 template 里的内容时)style-update
: 样式更新style-remove
: 样式移除js-update
: js 文件更新full-reload
: fallback 机制,网页重刷新其中针对 Vue 组件本身的一些更新,都可以直接调用 HMRRuntime
提供的方法,非常方便。其余的更新逻辑,基本上都是利用了 timestamp
刷新缓存重新执行的方法来达到更新的目的。
核心逻辑如下,我感觉非常清晰明了:
1 | import { HMRRuntime } from 'vue' // 来自 Vue3.0 的 HMRRuntime |
服务端的实现位于 src/node/serverPluginHmr.ts
。核心是监听项目文件的变更,然后根据不同文件类型(目前只有 vue
和 js
)来做不同的处理:
1 | watcher.on('change', async (file) => { |
对于 Vue
文件的热更新而言,主要是重新编译 Vue
文件,检测 template
、script
、style
的改动,如果有改动就通过 WS 服务端发起对应的热更新请求。
简单的源码分析如下:
1 | async function handleVueReload( |
对于热更新 js
文件而言,会递归地查找引用这个文件的 importer
。比如是某个 Vue
文件所引用了这个 js
,就会被查找出来。假如最终发现找不到引用者,则会返回 hasDeadEnd: true
。
1 | const vueImporters = new Set<string>() // 查找并存放需要热更新的 Vue 文件 |
如果 hasDeadEnd
为 true
,则直接发送 full-reload
。如果 vueImporters
或 jsHotImporters
里查找到需要热更新的文件,则发起热更新通知:
1 | if (hasDeadEnd) { |
写到这里,还有一个问题是,我们在自己的代码里并没有引入 HRM
的 client
代码,Vite 是如何把 client
代码注入的呢?
回到上面的一张图,Vite 重写 App.vue
文件的内容并返回时:
注意这张图里的代码区第一句话 import { updateStyle } from '/@hmr'
,并且在左侧请求列表中也有一个对 @hmr
文件的请求。这个请求是啥呢?
可以发现,这个请求就是上面说的客户端逻辑的 client.ts
的内容。
在 src/node/serverPluginHmr.ts
里,有针对 @hmr
文件的解析处理:
1 | export const hmrClientFilePath = path.resolve(__dirname, './client.js') |
至此,热更新的整体流程已经解析完毕。
这个项目最近在以惊人的速度迭代着,因此没过多久以后再回头看这篇文章,可能代码、实现已经过时。不过 Vite 的整体思路是非常棒的,在早期源码不多的情况下,能学到更贴近作者原始想法的东西,也算是很不错的收获。希望本文能给你学习 Vite 一些参考,有错误也欢迎大家指出。
]]>