時(shí)數(shù)據(jù)庫(kù)Lark:Firebase SDK兼容的自托管替代方案)
幾年前一個(gè)朋友聊起他們產(chǎn)品時(shí)提到一個(gè)兩難選擇客戶端實(shí)時(shí)協(xié)作功能用 Firebase 做開發(fā)確實(shí)是快監(jiān)聽數(shù)據(jù)、增量同步、離線隊(duì)列都內(nèi)置在 SDK 里團(tuán)隊(duì)甚至不需要維護(hù)后端。但業(yè)務(wù)做到一定規(guī)模后數(shù)據(jù)合規(guī)、賬單成本、定制化需求接踵而來(lái)他們才發(fā)現(xiàn) Firebase 這條路很難往回退。換數(shù)據(jù)庫(kù)不是換存儲(chǔ)而是要把所有查詢、監(jiān)聽、離線邏輯重寫一遍。所以當(dāng)我在 Hacker News 上看到 Show HN: Lark, OSS realtime database, drop-in compatible with Firebase SDKs 這個(gè)項(xiàng)目時(shí)第一反應(yīng)是這正是 Firebase 開發(fā)者一直缺的那條退路。用熟悉的 Firebase API把后端換成開源自托管。聽起來(lái)很理想但這類“兼容層”項(xiàng)目真正的風(fēng)險(xiǎn)從來(lái)不在能不能跑通而在邊界到底在哪里。1. 先搞清楚 Lark 這個(gè)定位到底解決了什么問(wèn)題1.1 用 Firebase 最爽的地方正是最容易被鎖定的地方Firebase Realtime Database 和 Firestore 吸引人的地方不只是“數(shù)據(jù)庫(kù)在云端”這么簡(jiǎn)單。它最核心的能力是實(shí)時(shí)監(jiān)聽客戶端可以訂閱某個(gè)路徑數(shù)據(jù)一變推送就到達(dá)。這個(gè)能力在后端需要維護(hù) WebSocket 連接、心跳、斷線重連和增量同步邏輯自己實(shí)現(xiàn)不是不行而是工作量極大。代價(jià)是你的數(shù)據(jù)模型、監(jiān)聽方式、離線同步策略都和 Firebase 的服務(wù)端深度綁定。Firebase 提供的是“托管服務(wù) 客戶端 SDK”這一整套東西你很難把客戶端 SDK 單獨(dú)抽出來(lái)指向自己的服務(wù)器。就算你導(dǎo)出了所有數(shù)據(jù)應(yīng)用程序的讀寫邏輯也已經(jīng)長(zhǎng)成了 Firebase 的形狀。Lark 想改變的點(diǎn)不是“數(shù)據(jù)庫(kù)性能更強(qiáng)”而是“API 不換后端換成開源的”。開發(fā)者不需要丟掉 Firebase SDK 的用法只要把原來(lái)指向 Firebase 服務(wù)的連接配置替換成指向自托管服務(wù)的地址。這就是標(biāo)題里 “drop-in compatible” 的含義盡可能減少對(duì)現(xiàn)有代碼的改動(dòng)。注意這里的 Lark 和字節(jié)跳動(dòng)的飛書海外版同樣叫 Lark不是同一個(gè)東西。搜索資料時(shí)很容易混入無(wú)關(guān)內(nèi)容這是一個(gè)很容易踩的認(rèn)知干擾點(diǎn)。1.2 “SDK 兼容”和“協(xié)議兼容”是兩條不同的路要理解這類項(xiàng)目得先分清兩個(gè)概念。SDK 兼容提供一套和 Firebase SDK 相同或相似的客戶端接口開發(fā)者的調(diào)用方式不變但底層可能是另一個(gè)協(xié)議的封裝。協(xié)議兼容在客戶端 SDK 原樣不動(dòng)的前提下服務(wù)端實(shí)現(xiàn)了 Firebase 的 wire protocol原版 SDK 可以直接通過(guò)改配置連上。從 Lark 的標(biāo)題看它寫的是 “compatible with Firebase SDKs”更像前者。這意味著項(xiàng)目可能在客戶端 SDK 上做適配也可能重新實(shí)現(xiàn)了服務(wù)端協(xié)議。兩種路線的維護(hù)成本、兼容程度、升級(jí)節(jié)奏都不一樣。如果只是 API 名字一樣但底層協(xié)議對(duì)不上那么一些隱藏行為會(huì)被擾動(dòng)重連時(shí)的數(shù)據(jù)恢復(fù)、離線緩存的合并策略、安全規(guī)則的執(zhí)行方式。這些很難通過(guò)幾個(gè)簡(jiǎn)單 demo 暴露出來(lái)。所以在評(píng)估 Lark 時(shí)不要只看“代碼能不能編譯”。要看它是怎么實(shí)現(xiàn)兼容的以及這種兼容到了哪一層。1.3 這類項(xiàng)目真正的價(jià)值是降低遷移成本不是取代 FirebaseLark 不會(huì)讓 Firebase 變差也不會(huì)讓自托管突然變成所有團(tuán)隊(duì)的標(biāo)配。它真正提供的是“遷移成本”上的一個(gè)選項(xiàng)。Firebase 對(duì)很多團(tuán)隊(duì)來(lái)說(shuō)依然是效率極高的方案尤其是中小型項(xiàng)目、內(nèi)部工具和快速原型。它的短板是數(shù)據(jù)主權(quán)數(shù)據(jù)存儲(chǔ)在 Google 的托管設(shè)施上合規(guī)要求、網(wǎng)絡(luò)條件、賬單策略都可能成為變量。Lark 這類兼容層的意義在于你不再只能在“開發(fā)效率”和“數(shù)據(jù)自主權(quán)”之間二選一。你可以繼續(xù)用熟悉的 API 開發(fā)再把后端放到自己的基礎(chǔ)設(shè)施上。它比單純說(shuō)“替代 Firebase”更準(zhǔn)確也更容易落地。這個(gè)角度的好處是你不需要成為自托管激進(jìn)派也能理解它的存在價(jià)值多一條退路選型時(shí)多一個(gè)自由度。2. 當(dāng)它說(shuō)“兼容 Firebase SDKs”時(shí)你要驗(yàn)證的是哪些層2.1 第一層客戶端 API 的命名和簽名最直觀的驗(yàn)證是客戶端代碼能不能少改動(dòng)地跑起來(lái)。比如 Firebase JS SDK 里有這樣的寫法import { initializeApp } from firebase/app; import { getDatabase, ref, onValue, set } from firebase/database; const app initializeApp({ apiKey: ..., databaseURL: https://your-project-default-rtdb.firebaseio.com }); const db getDatabase(app); const userRef ref(db, users/123); onValue(userRef, (snapshot) { console.log(snapshot.val()); }); await set(userRef, { name: Alice });如果 Lark 是 API 層兼容這段代碼可能只需要把databaseURL換成自托管地址或者換成 Lark 提供的初始化配置。但如果它連返回類型都變了后續(xù)的.val()、snapshot.exists()、錯(cuò)誤對(duì)象結(jié)構(gòu)就可能全部走樣。從工程經(jīng)驗(yàn)看API 兼容最容易忽略的是返回類型。名字一樣返回的是 Promise、自定義對(duì)象還是原始引用直接影響await、.then和錯(cuò)誤捕獲邏輯。建議把項(xiàng)目中高頻使用的 API 寫成一個(gè)腳本逐個(gè)驗(yàn)證返回值和異常行為。2.2 第二層實(shí)時(shí)同步模型實(shí)時(shí)數(shù)據(jù)庫(kù)的核心是“數(shù)據(jù)變了客戶端馬上收到通知”。要驗(yàn)證的不只是“能不能收到”還有“收到的東西對(duì)不對(duì)”。具體來(lái)說(shuō)重點(diǎn)看這幾點(diǎn)數(shù)據(jù)變更后監(jiān)聽回調(diào)的觸發(fā)是否及時(shí)監(jiān)聽順序是否穩(wěn)定斷網(wǎng)重連后能不能恢復(fù)斷線期間錯(cuò)過(guò)的增量多個(gè)客戶端同時(shí)寫入時(shí)事件順序是否一致刪除、更新、嵌套路徑變化時(shí)返回的數(shù)據(jù)結(jié)構(gòu)是否符合預(yù)期。這些行為在單元測(cè)試?yán)锖茈y覆蓋通常要模擬弱網(wǎng)、斷線、多端并發(fā)才有體感。如果只跑一次 happy path你只會(huì)看到“數(shù)據(jù)同步了”看不到斷線重連后的亂序或丟失問(wèn)題。如果你做的是聊天、協(xié)同編輯、實(shí)時(shí)狀態(tài)同步這類強(qiáng)實(shí)時(shí)應(yīng)用這一層必須重點(diǎn)壓測(cè)。2.3 第三層認(rèn)證與安全規(guī)則Firebase 的一大特色是安全規(guī)則。它允許你在服務(wù)端聲明式地定義“誰(shuí)能讀、誰(shuí)能寫、什么條件下允許”而不是把權(quán)限判斷全堆在客戶端代碼里。例如Firestore 規(guī)則大致長(zhǎng)這樣match /users/{userId} { allow read, write: if request.auth.uid userId; }Lark 這類項(xiàng)目是否支持近似規(guī)則決定了你的權(quán)限模型能不能直接遷移。這里要確認(rèn)三件事它是否支持 Firebase Authentication 的 ID token 校驗(yàn)token 是直接用 Firebase 的還是 Lark 自己發(fā)如果客戶端已經(jīng)接了 Google 登錄、匿名登錄、自定義 token后端能不能識(shí)別并校驗(yàn)。如果項(xiàng)目是純內(nèi)部工具、無(wú)多租戶認(rèn)證可以先簡(jiǎn)化。但如果面向公眾用戶安全規(guī)則就不是“上線前再補(bǔ)”的問(wèn)題而是開始設(shè)計(jì)數(shù)據(jù)模型時(shí)就要同步考慮的東西。2.4 第四層離線緩存與沖突處理Firebase 客戶端 SDK 的另一個(gè)招牌能力是離線緩存。網(wǎng)絡(luò)斷開時(shí)寫入會(huì)進(jìn)入本地隊(duì)列恢復(fù)后自動(dòng)同步讀取過(guò)的數(shù)據(jù)會(huì)留在本地離線時(shí)也能讀到緩存版本。自托管兼容層能不能實(shí)現(xiàn)同樣的離線行為決定了弱網(wǎng)環(huán)境下的可用性。很多“兼容”項(xiàng)目在線場(chǎng)景表現(xiàn)不錯(cuò)一旦斷開網(wǎng)絡(luò)行為就完全不同。常見問(wèn)題包括離線寫入沒(méi)有被排隊(duì)恢復(fù)后隊(duì)列沒(méi)有正確上傳離線讀取直接報(bào)錯(cuò)多個(gè)客戶端離線修改同一字段沖突處理策略不一致。如果產(chǎn)品有大量移動(dòng)端用戶這一層必須專門測(cè)。只做 Web 端、網(wǎng)絡(luò)穩(wěn)定的內(nèi)部工具可以相對(duì)放寬。3. 從“能連上”到“能上線”自托管數(shù)據(jù)庫(kù)的踩坑路徑3.1 環(huán)境準(zhǔn)備和最小跑通在 Lark 官方文檔不明確的情況下我的建議是先看它是否提供 Docker 鏡像或編譯好的服務(wù)端。常見的自托管路徑是# 先拉取鏡像再設(shè)置存儲(chǔ)目錄和端口 docker pull your-registry/lark-server:latest docker run -d -p 8080:8080 \ -v ./data:/data \ your-registry/lark-server注意上面的鏡像名和參數(shù)只是通用示例。Lark 當(dāng)前如果還沒(méi)有官方鏡像就要先按倉(cāng)庫(kù) README 編譯服務(wù)端。別在沒(méi)確認(rèn)依賴版本前照著網(wǎng)上的參數(shù)直接套。為什么堅(jiān)持先跑最小 Demo因?yàn)閷?shí)時(shí)數(shù)據(jù)庫(kù)的問(wèn)題通常不在“能不能啟動(dòng)”而在“數(shù)據(jù)能不能流動(dòng)”。先確認(rèn)服務(wù)端能啟動(dòng)、能連上、能寫入并讀回一條完整數(shù)據(jù)再往真實(shí)業(yè)務(wù)走。3.2 不要把本地跑通當(dāng)作生產(chǎn)可用本地跑通只能說(shuō)明代碼路徑?jīng)]有斷。從自托管到生產(chǎn)還差三件事持久化存儲(chǔ)容器重啟后數(shù)據(jù)不能丟。Docker 里要掛載 volume不能把數(shù)據(jù)放在容器可寫層。備份和恢復(fù)數(shù)據(jù)目錄、數(shù)據(jù)庫(kù)快照、恢復(fù)演練都要有明確的流程。監(jiān)控和告警連接數(shù)、存儲(chǔ)增長(zhǎng)、慢查詢、錯(cuò)誤日志至少要有一個(gè)面板或日志采集。如果只是做小項(xiàng)目或內(nèi)部工具這三件事可以簡(jiǎn)化只要保證數(shù)據(jù)有一個(gè)備份、進(jìn)程掛掉能被重新拉起就行。但如果你想把它當(dāng)作團(tuán)隊(duì)基礎(chǔ)設(shè)施就得按生產(chǎn)標(biāo)準(zhǔn)來(lái)。3.3 先驗(yàn)證數(shù)據(jù)模型和查詢模式Firebase 的文檔型數(shù)據(jù)庫(kù)有個(gè)特點(diǎn)業(yè)務(wù)模型通常避免復(fù)雜 JOIN而是用嵌套結(jié)構(gòu)或反規(guī)范化來(lái)組織數(shù)據(jù)。遷移到自托管兼容層后查詢能力是否一致非常關(guān)鍵。把現(xiàn)有應(yīng)用里最常用的數(shù)據(jù)庫(kù)操作列成清單逐項(xiàng)驗(yàn)證基礎(chǔ)寫入、更新、刪除按字段過(guò)濾排序和分頁(yè)集合級(jí)和文檔級(jí)監(jiān)聽批量寫入或事務(wù)嵌套路徑的讀寫大數(shù)據(jù)量下的讀取延遲。不要只在管理后臺(tái)里手動(dòng)寫數(shù)據(jù)要真的從客戶端請(qǐng)求里模擬。真實(shí)應(yīng)用的壓力來(lái)自查詢模式而不是數(shù)據(jù)庫(kù)里存了多少行。3.4 安全、備份、監(jiān)控是繞不開的三件事自托管意味著你要自己承擔(dān)安全責(zé)任。至少要做到關(guān)閉服務(wù)的默認(rèn)公開訪問(wèn)設(shè)置鑒權(quán) token、密鑰或網(wǎng)絡(luò)策略限制 CORS 來(lái)源定期檢查依賴更新和已知漏洞。另外如果你所在的公司有開源合規(guī)流程把 Lark 拉進(jìn)依賴掃描列表是很有必要的。類似 Black Duck 這類工具掃描后會(huì)自動(dòng)提示許可證和已知漏洞風(fēng)險(xiǎn)。代碼能跑起來(lái)是一回事合規(guī)能不能通過(guò)是另一回事。4. 它適合什么場(chǎng)景又不適合什么場(chǎng)景4.1 適合先試水的場(chǎng)景下面幾類情況把 Lark 列入備選是合理的已經(jīng)有 Firebase 代碼但想降低對(duì)單一托管服務(wù)的依賴項(xiàng)目處于原型階段想快速用熟悉的 API 搭建實(shí)時(shí)功能對(duì)數(shù)據(jù)主權(quán)、合規(guī)、隱私要求高必須把數(shù)據(jù)放在自己服務(wù)器上團(tuán)隊(duì)有后端運(yùn)維能力愿意為數(shù)據(jù)自主權(quán)花一些成本想深入學(xué)習(xí)實(shí)時(shí)數(shù)據(jù)庫(kù)和同步機(jī)制的人。在這些場(chǎng)景里“低遷移成本”是很實(shí)在的優(yōu)勢(shì)。你不需要先學(xué)一套新數(shù)據(jù)庫(kù)概念再設(shè)計(jì)一套新數(shù)據(jù)模型而是可以沿用已有的 Firebase 使用經(jīng)驗(yàn)。4.2 不建議直接用的場(chǎng)景下面幾類情況建議謹(jǐn)慎項(xiàng)目規(guī)模已經(jīng)很大依賴了很多 Firebase 高級(jí)特性比如云函數(shù)、擴(kuò)展、App Check、ML Kit。這些和數(shù)據(jù)庫(kù)內(nèi)核無(wú)關(guān)兼容層通常不會(huì)覆蓋。需要極高的性能和 SLA而團(tuán)隊(duì)沒(méi)有自托管運(yùn)維經(jīng)驗(yàn)。開源自托管意味著你自己負(fù)責(zé)升級(jí)、監(jiān)控、故障恢復(fù)。對(duì)數(shù)據(jù)一致性要求極其嚴(yán)格需要強(qiáng)事務(wù)、復(fù)雜回滾、SQL 級(jí)聚合查詢。文檔型實(shí)時(shí)數(shù)據(jù)庫(kù)的核心設(shè)計(jì)就不側(cè)重這個(gè)。團(tuán)隊(duì)規(guī)模很小只想“一個(gè)服務(wù)省事”那 Firebase 本身依然更合適。兼容層項(xiàng)目通常解決“能不能用”而不是“和 Firebase 完全一樣”。把 drop-in 理解成“零成本遷移”會(huì)有風(fēng)險(xiǎn)。4.3 用一張驗(yàn)證清單代替拍腦袋具體可以用下面這張表來(lái)驗(yàn)證兼容情況驗(yàn)證項(xiàng)方法通過(guò)標(biāo)準(zhǔn)API 兼容把常用 API 寫成一個(gè)腳本逐項(xiàng)調(diào)用無(wú)編譯錯(cuò)誤返回值和類型正常實(shí)時(shí)推送兩個(gè)客戶端同時(shí)監(jiān)聽一端寫入一端觀察無(wú)需刷新也能收到更新離線重連斷開網(wǎng)絡(luò) 10 秒再恢復(fù)客戶端能恢復(fù)并收到重連期間的變化認(rèn)證用現(xiàn)有登錄流程獲取 token 后讀寫規(guī)則按預(yù)期放行或攔截安全規(guī)則用未授權(quán) token 訪問(wèn)受保護(hù)路徑請(qǐng)求被拒絕持久化重啟服務(wù)端容器數(shù)據(jù)不丟失備份恢復(fù)手動(dòng)導(dǎo)出數(shù)據(jù)再恢復(fù)數(shù)據(jù)完整關(guān)聯(lián)關(guān)系正常這七項(xiàng)跑下來(lái)基本能判斷一個(gè)自托管實(shí)時(shí)數(shù)據(jù)庫(kù)適不適合你的項(xiàng)目。4.4 許可證和開源合規(guī)也值得提前看一眼開源實(shí)時(shí)數(shù)據(jù)庫(kù)的許可證決定了你能不能商用、要不要開源自己的修改、能不能嵌入到閉源產(chǎn)品里。不同項(xiàng)目常用 MIT、Apache 2.0、AGPL、BSL 等許可證。AGPL 對(duì)互操作性和網(wǎng)絡(luò)服務(wù)有特殊要求如果做閉源 SaaS就要特別留意。同樣重要的是項(xiàng)目活躍度。一個(gè)只有幾顆星、幾個(gè)月不更新的項(xiàng)目功能再多也不敢放到生產(chǎn)???issues 的響應(yīng)速度、commit 頻率、維護(hù)者是否還在處理 PR比看 README 里的功能列表更實(shí)在。開源合規(guī)掃描和社區(qū)健康度檢查應(yīng)該成為選型的前置步驟。5. 如果我想試用 Lark該怎么開始5.1 先跑最小 Demo而不是直接重構(gòu)不要一開始就把整個(gè) Firebase 項(xiàng)目切過(guò)去。更穩(wěn)妥的順序是單獨(dú)建一個(gè)測(cè)試項(xiàng)目用 Lark 跑一個(gè)“頻道 消息”的實(shí)時(shí)聊天或通知 Demo用 Firebase SDK 初始化并寫入幾條數(shù)據(jù)開兩個(gè)客戶端驗(yàn)證實(shí)時(shí)更新模擬斷網(wǎng)和重連關(guān)閉服務(wù)端再啟動(dòng)看數(shù)據(jù)是否還在。這一步的目的不是證明產(chǎn)品功能而是幫你理解它的架構(gòu)。只有自己跑一遍你才知道啟動(dòng)方式、依賴、配置項(xiàng)有哪些。比如用 Node.js 寫一個(gè)最小驗(yàn)證腳本思路是// 這是驗(yàn)證思路不是特定于 Lark 的完整代碼 // 重點(diǎn)是看 SDK 的初始化、監(jiān)聽、寫入是否能在自托管地址上工作 import { initializeApp } from firebase/app; import { getDatabase, ref, set, onValue } from firebase/database; const app initializeApp({ apiKey: test-key, databaseURL: http://localhost:8080 // 自托管服務(wù)地址 }); const db getDatabase(app); const testRef ref(db, demo/message); await set(testRef, hello from self-hosted); onValue(testRef, (snapshot) { console.log(received:, snapshot.val()); });這個(gè)腳本只驗(yàn)證一條鏈路初始化、寫入、監(jiān)聽。如果這一步都卡住后面的業(yè)務(wù)遷移就先別開始。5.2 用一張“兼容性核對(duì)表”做驗(yàn)收真實(shí)遷移前把現(xiàn)有 Firebase API 調(diào)用全部列出來(lái)按“高頻依賴”和“低頻使用”排序然后逐個(gè)測(cè)試初始化方式和連接參數(shù)是否有變化數(shù)據(jù)寫入、讀取、刪除的行為是否一致監(jiān)聽事件的觸發(fā)時(shí)機(jī)和數(shù)據(jù)內(nèi)容是否一致離線隊(duì)列是否生效錯(cuò)誤碼和異常結(jié)構(gòu)是否相同認(rèn)證 token 的校驗(yàn)和權(quán)限判斷是否等價(jià)復(fù)雜查詢、排序、分頁(yè)是否行為一致。如果高頻 API 都通過(guò)遷移風(fēng)險(xiǎn)就低。如果高頻 API 里有幾個(gè)行為不一致就要評(píng)估改造成本而不是硬切。提醒不要一上來(lái)就在生產(chǎn)環(huán)境直接切換。先做一段時(shí)間的并行驗(yàn)證把真實(shí)流量復(fù)制到自托管服務(wù)上觀察穩(wěn)定性和數(shù)據(jù)一致性再?zèng)Q定是否切換。5.3 最終看三個(gè)指標(biāo)遷移成本、運(yùn)行穩(wěn)定性、維護(hù)可承受度判斷一個(gè)兼容層方案值不值得長(zhǎng)期用最終看三個(gè)指標(biāo)遷移成本現(xiàn)有代碼要改多少學(xué)習(xí)新配置的成本多高。如果改造成本接近重寫那兼容層的意義就消失了。運(yùn)行穩(wěn)定性上線后連續(xù)運(yùn)行是否穩(wěn)定重連、并發(fā)、數(shù)據(jù)一致性是否可靠。這個(gè)只有長(zhǎng)時(shí)間壓測(cè)和試運(yùn)行才能驗(yàn)證。維護(hù)可承受度Lark 自身升級(jí)、依賴更新、故障排查你是否有能力和精力承接。開源軟件的維護(hù)成本往往在半年后才會(huì)顯現(xiàn)。這三個(gè)指標(biāo)組合起來(lái)才能看出一個(gè)方案是不是真的在“開發(fā)效率”和“數(shù)據(jù)自主權(quán)”之間取得平衡。如果遷移成本低但穩(wěn)定性不行長(zhǎng)期看還是要返工如果功能很強(qiáng)但運(yùn)維太重小團(tuán)隊(duì)可能被拖垮。像 Lark 這樣的開源實(shí)時(shí)數(shù)據(jù)庫(kù)短期內(nèi)不會(huì)讓 Firebase 過(guò)時(shí)也不會(huì)讓所有團(tuán)隊(duì)都轉(zhuǎn)向自托管。它更重要的意義是提供了一個(gè)“還可以這么做”的選項(xiàng)用 Firebase 的 API保留開源的數(shù)據(jù)層。這個(gè)選項(xiàng)讓技術(shù)選型不再是非黑即白的二選一而是變成了按場(chǎng)景、按合規(guī)、按團(tuán)隊(duì)能力來(lái)權(quán)衡的動(dòng)態(tài)選擇。如果你正在被 Firebase 的鎖定問(wèn)題困擾與其急著重構(gòu)不如先花一個(gè)下午把這類兼容層項(xiàng)目跑通再嚴(yán)格驗(yàn)證一遍實(shí)時(shí)性、認(rèn)證、緩存和持久化。結(jié)果無(wú)非兩種要么發(fā)現(xiàn)它還不成熟那你會(huì)更清楚 Firebase 在哪些地方不可替代要么發(fā)現(xiàn)它確實(shí)夠用那你手里的自由度就多了一塊。這兩種結(jié)果對(duì)你都是收益。