未來會怎樣構建Web應用程序?

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"本文最初發佈於stopa.io網站,經原作者授權由InfoQ中文站翻譯並分享。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在未來,我們會怎樣構建Web應用程序呢?如果行業正常發展下去的話,那麼今天我們認爲很難、做起來很有價值的事情在明天都會變得很輕鬆普遍。我想我們會發現很多新的抽象,讓"},{"type":"link","attrs":{"href":"https:\/\/baike.baidu.com\/item\/Google%20Docs\/9788863?fr=aladdin","title":"","type":null},"content":[{"type":"text","text":"Google Docs"}]},{"type":"text","text":"寫起來也能像今天的普通Web應用一樣簡單。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這就引出來一個問題——這些抽象會是什麼樣子?我們今天能發現它們嗎?想要找出答案,一種方法是審視我們在構建Web應用程序時必須經歷的所有問題,然後看看我們能做些什麼。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"親愛的讀者,這篇文章就是我對上述方法的一次實踐嘗試。我們會走過一段旅程,看看今天我們是如何構建Web應用程序的:我們將回顧行業面臨的各種問題,評估"},{"type":"link","attrs":{"href":"https:\/\/baike.baidu.com\/item\/Firebase\/19675247?fr=aladdin","title":"","type":null},"content":[{"type":"text","text":"Firebase"}]},{"type":"text","text":"、Supabase、Hasura等解決方案,看看還有什麼需要做的事情。我想到了旅途的最後,你一定會同意我的觀點,那就是瀏覽器中的數據庫看起來應該是最有用的抽象之一。不過,這裏說的有點太遠了,我們先從頭開始。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"客戶端"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這段旅程始於瀏覽器中的"},{"type":"link","attrs":{"href":"https:\/\/www.w3school.com.cn\/js\/js_intro.asp","title":"","type":null},"content":[{"type":"text","text":"Javascript"}]},{"type":"text","text":"。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"A.數據管道"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們的第一步工作是獲取信息並將其顯示在各個位置。例如,我們可能會顯示一個好友列表、好友數量、特定好友組的一個模態等。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們面臨的問題是,所有組件看到的信息都需要是一致的。如果一個組件看到的好友數據和別的不一樣,你就可能顯示出錯誤的“計數”,或者一個視圖與另一個視圖中的暱稱不一樣。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲解決這個問題,我們需要有一個核心的事實來源。於是每當我們獲取什麼東西時,我們都會對其標準化並把它放在一個地方(通常是一個存儲)。然後,每個組件(使用一個選擇器)讀取並轉換所需的數據。下面這樣的代碼是很常見的:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"\/\/ normalise [posts] -> {[id]: post}\nfetchRelevantPostsFor(user).then(posts => {\n posts.forEach(post => {\n store.addPost(post);\n })\n})\n\n\/\/ see all posts by author: \nstore.posts.values().reduce((res, post) => { \n res[post.authorId] = res[post.authorId] || []; \n res[post.authorId].push(post);\n return res;\n}, {})"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏的問題是,爲什麼我們需要做這些工作呢?我們得編寫自制代碼來處理這些數據,可是數據庫早就解決這個問題了。我們應該能夠“查詢”數據纔是,比如說:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"SELECT posts WHERE post.author_id = ?;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這樣查詢我們瀏覽器內部的信息不是很方便嗎?"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"B.更改"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下一個問題是讓數據保持最新狀態。假設我們刪除了一個好友,會發生什麼呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們發送一個API請求,等待它完成,然後編寫一些邏輯來“刪除”關於這個好友的所有信息。比如這樣的代碼:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"deleteFriend(user, friend.id).then(res => { \n userStore.remove(friend.id);\n postStore.removeUserPosts(friend.id);\n})"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但這種機制很快就會變得很麻煩:我們必須記住存儲中可能受這一更改影響的所有位置纔行,就好像我們要在大腦裏搞一個垃圾收集器,可我們的大腦不擅長這種活兒。爲了避開它,人們想出的一種辦法是跳過問題並重新獲取整個世界:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"deleteFriend(user, id).then(res => {\n fetchFriends(user);\n fetchPostsRelevantToTheUser(post);\n})"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這兩種解決方案都不是很好。在這兩種情況下都存在我們需要留意的隱式不變量(基於這一更改,我們還需要注意其他哪些更改?),並且我們在應用程序中引入了延遲。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"問題是,當我們對數據庫做任何更改時,它用不着我們這麼小心就可以完成工作。爲什麼瀏覽器不能自動搞定這種事情呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"DELETE FROM friendships WHERE friend_one_id = ? AND friend_two_id = ?\n-- Browser magically updates with all the friend and post information removed"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"C.樂觀更新"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"你可能已經注意到B.的問題是,我們必須等待好友被移除才能更新瀏覽器狀態。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在大多數情況下,我們可以通過一個樂觀更新來加快速度——畢竟,我們知道調用很可能會成功。爲此,我們執行以下操作:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"friendPosts = userStore.getFriendPosts(friend);\nuserStore.remove(friend.id);\npostStore.removeUserPosts(friend.id);\ndeleteFriend(user, id).catch(e => { \n \/\/ undo\n userStore.addFriend(friend);\n postStore.addPosts(friendPosts);\n})"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這更煩人了。現在我們需要手動更新成功操作和失敗操作纔行。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這是爲什麼?在後端,數據庫本來就能做樂觀更新啊——爲什麼我們不能在瀏覽器中這樣做?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"DELETE friendship WHERE friend_one_id = ? AND friend_two_id = ?\n-- local store optimistically updated, if operation fails we undo"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"D.響應性"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"數據不僅會因我們自己的行爲而改變。有時我們需要連接到其他用戶所做的更改。例如,有人可以取消我們的好友關係,或者有人可以向我們發送消息。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了完成這項工作,我們需要做的事情與在API端點中所做的是一樣的,但這次是在我們的websocket連接上:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"ws.listen(`${user.id}\/friends-removed`, friend => { \n userStore.remove(friend.id);\n postStore.removeUserPosts(friend.id);\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但這又引入兩個問題。首先,我們又得玩垃圾收集器那套了,需要記住可能受事件影響的每一個位置。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其次,如果我們要做樂觀更新,我們就會遇到爭用情況。想象一下,你運行一個樂觀更新,將一個形狀的顏色設置爲"},{"type":"codeinline","content":[{"type":"text","text":"blue"}]},{"type":"text","text":",同時一個陳舊(stale)更新跑來了,說它是"},{"type":"codeinline","content":[{"type":"text","text":"red"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"Optimistic Update: `Blue`\nStale reactive update: `Red`\nSuccessful Update, comes in through socket: `Blue`"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現在你會看到閃爍的圖像。樂觀更新把形狀改成藍色,響應更新又會把它改成紅色,但是一旦樂觀更新成功,新的響應更新又會把它變回藍色。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"解決這樣的問題涉及一致性的主題,於是你會去搜索關於……數據庫的資料。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其實,用不着這麼麻煩。如果每個查詢都是響應式的呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"SELECT friends FROM users JOIN friendships on friendship.user_one_id = ?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現在,好友關係的任何變化都會自動更新訂閱這個查詢的視圖。你不必操心哪些內容出現了更改,並且你的本地數據庫可以找出“最新更新”的內容,於是消除了大部分複雜性。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"服務器"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在服務器上,問題只會更復雜。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"E.端點"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"許多後端開發工作到頭來成爲了數據庫和前端之間的一種粘合劑。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"\/\/ db.js\nfunction getRelevantPostsFor(userId) { \n db.exec(\"SELECT * FROM users WHERE ...\")\n}\n\n\/\/ api.js\napp.get(\"relevantPosts\", (req, res) => { \n res.status(200).send(getRelevantPosts(req.userId));\n}) "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏面也太多重複了,以至於我們最後要創建腳本來生成這些文件。但是爲什麼我們需要這樣做呢?不管怎樣,它們通常是與客戶端非常緊密地耦合的。爲什麼我們不能直接將數據庫暴露給客戶端呢?"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"F.權限"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"好吧,我們不這樣做的原因是我們需要確保權限正確設置。例如,你應該只能看到你好友的帖子。爲此,我們向API端點添加中間件:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"app.put(\"user\", auth, (req, res) => {\n...\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但這會變得越來越混亂。Websocket呢?新的代碼更改有時會引入一些你意想不到的方法來更新數據庫對象。突然之間,你就遇到了麻煩。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏要問的問題是,爲什麼要在API級別進行身份驗證?理想情況下,我們應該有一些非常接近數據庫的東西,確保任何數據訪問都通過權限檢查。像Postgres這樣的數據庫有行級安全性,但這很快就會變得很麻煩。但如果你能“描述”數據庫附近的實體呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"User { \n view: [\n IAllowIfAdmin(),\n IAllowIfFriend(),\n IAllowIfSameUser(),\n ]\n write: [\n IAllowIfAdmin(),\n IAllowIfSameUser(),\n ]\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在這裏,我們編寫一些身份驗證規則,並確保不管你嘗試用哪種方式來編寫和更新用戶實體,你都可以被許可。於是乎,現在只有少數代碼更改(而不是大多數更改)會影響權限了。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"G.審計、撤消\/重做"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"並且在某些時候,我們要完成的需求會增加複雜性。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"例如,假設我們需要支持“撤消\/重做”,用於好友操作。一個用戶刪除了一個好友,然後他們按下了“撤消”——我們怎麼來支持這一過程呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們不能直接刪除好友關係,因爲如果我這樣做的話,就沒法不知道這個人原本“已經是好友”,還是現在剛請求成爲好友。在後一種情況下,我們可能需要發送好友請求才行。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了解決這個問題,我們改進了數據模型。我們將用“好友事實”來代替單一的好友關係。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"[\n{status: \"friends\", friend_one_id: 1, friend_two_id: 2, at: 1000},\n{status: \"disconnected\", friend_one_id: 1, friend_two_id: 2, at: 10001},\n]"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼“最新事實”會代表倆人之間是否存在好友關係。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這種辦法是可行的,但大多數數據庫並不是爲它設計的:查詢不像我們預期的那樣工作,優化起來也比我們預期的更難。我們最後不得不非常小心地處理更新機制,以免意外刪除記錄。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"突然之間,我們變成了“某種數據庫工程師”,跑去大量查閱有關查詢優化的資料。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這種要求看似獨特,但在實踐中越來越常見。如果你處理的是金融交易,你需​​要這樣的機制來做審計。撤消\/重做是許多應用中的必需品。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"也許突然發生了一個錯誤,於是我們不小心刪除了數據。在事實統治的世界中不會有這樣的事情——反正你可以撤銷刪除操作。但這並不是我們大多數人生活的世界。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有一些模式將事實視爲一等公民(Datomic,後文具體討論),但現在它們還是很罕見的,很少有工程師能做到。如果這種模式沒那麼罕見呢?"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"H.離線模式"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"令人頭疼的例子還有很多。比如說離線模式——許多應用程序都是長期運行的,可以在沒有互聯網連接的情況下繼續運行一段時間。我們如何支持這一特性呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們只能再次進化我們的數據模型,但這一次真正將所有內容都作爲“事實”,並準備一個客戶端數據庫,該數據庫基於這些事實來演進自己的內部狀態。恢復連接後,我們應該能夠協調更改。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這很難做到。從本質上講,能做到這一步的程序員都變成了數據庫工程師。但是,如果我們在瀏覽器中有一個數據庫,讓它扮演分佈式數據庫中的一個“節點”,上面的任務不就可以自動完成了嗎?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"事實證明,基於事實的系統實際上更容易做到這一點。許多人認爲我們需要求助於操作轉換來做這樣的事情,但正如figma展示的那樣,只要我們允許單一的領導者,並且可以接受最後寫入者獲勝這樣的語義,我們就可以徹底簡化這個機制,只要事實就足夠了。當你需要更嚴肅的解決方案時,你可以打開OT兔子洞。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"想象一下......立即啓用離線模式。這樣一來,大多數應用程序會變成什麼樣?"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"I.響應性"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前面,我們討論了來自客戶端的響應性。在服務器上的響應性也是個問題。我們必須確保在數據更改時更新所有相關客戶端。例如,如果添加了一個“帖子”,我們需要通知與這個帖子相關的所有可能訂閱。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"function addPost(post) {\n db.addPost(post);\n getAllFriends(post).forEach(notifyNewPost);\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這會變得相當混亂。我們很難知曉所有可能相關的主題。錯過一些主題也是很容易的:如果使用"},{"type":"codeinline","content":[{"type":"text","text":"addPost"}]},{"type":"text","text":"之外的查詢更新數據庫,我們永遠不會知道是不是有主題被錯過了。這項工作需要開發人員來完成。它開始做起來很容易,但會變得越來越複雜。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然而,數據庫也可以知曉所有這些訂閱,並且可以只處理更新相關的查詢。RethinkDB是在這方面做得很好的一個例子。如果你選擇的查詢語言可以做到這一點,是不是會很方便?"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"J.衍生數據"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最終,我們需要將數據放在多個位置:緩存(Redis)、搜索索引(ElasticSearch)或分析引擎(Hive)。這個步驟會變得非常麻煩。你可能需要引入某種隊列(Kafka),確保所有這些衍生源都保持最新狀態。這裏面的工作涉及配置機器、引入服務發現和整個shebang等操作。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可爲什麼要這麼複雜呢?在一個常規數據庫中,你可以執行以下操作:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"CREATE INDEX ..."}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於其他服務,我們爲什麼不能這樣做?Martin Kleppman在他的《數據密集型應用程序》中提出了這樣一種語言:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"db |> ElasticSearch\ndb |> Analytics\ndb.user |> Redis\n\/\/ Bam, we've connected elastic search, analytics, and redis to our db"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"破壞性因素"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們都列舉到了J。但這些只是你開始構建應用程序後纔開始面臨的問題。那麼在開始構建之前呢?"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"K.TTP——原型製作時間"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"也許今天對開發人員來說最難辦的問題是上手。如果你想存儲用戶信息並顯示一個頁面,你會怎麼做?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以前,你只需要一個"},{"type":"codeinline","content":[{"type":"text","text":"index.html"}]},{"type":"text","text":"和FTP就行了。現在,你需要webpack、typescript、大量的構建過程,經常還需要多個服務。活動的部件太多了,邁出第一步都絕非易事。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這似乎是一個菜鳥才需要面對的問題,似乎有經驗的程序員上手起來會快很多。我認爲情況更復雜一些。大多數項目都處於邊緣場景——它們不是你日常應對的那種類型。這意味着原型製作階段哪怕只多了幾分鐘,也可能會讓我們淘汰很多項目。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"簡化這一步驟將大大增加我們可以使用的應用程序數量。如果這一階段能比"},{"type":"codeinline","content":[{"type":"text","text":"index.html"}]},{"type":"text","text":"和FTP更容易完成呢?"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"當前的解決方案"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這問題可是真夠多的。情況看起來很糟糕,但如果你回過頭看看區區幾年前的樣子,​​就會發現我們已經有了這麼大的進步。不管怎樣,我們不再需要自己應付那些機架了。如同文藝復興時代一樣,很多傑出的人才正在努力開發這些問題的解決方案。這些方案有哪些代表呢?"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"Firebase"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我認爲Firebase在推動Web應用程序開發方面做了一些最具創新性的工作。他們做的最重要的一件事情就是"},{"type":"text","marks":[{"type":"strong"}],"text":"瀏覽器上的數據庫"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有了firebase,你可以像在服務器上一樣查詢數據。通過這種抽象,他們解決了上面列出的A-E問題。Firebase可以處理樂觀更新,默認就是響應式的。它提供了對權限的支持,從而消除了對端點的需求。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"K問題也可以從中大大獲益:我認爲它的原型製作速度表現還是市面上最出色的。你只需從"},{"type":"codeinline","content":[{"type":"text","text":"index.html"}]},{"type":"text","text":"開始就行了!"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但它也有兩個問題:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第一,查詢能力。Firebase選擇的文檔模型簡化了抽象管理,但會破壞你的查詢能力。很多時候,你必須對數據做反正則化,或者查詢變得很難處理。例如,要記錄像好友這樣的多對多關係,你需要執行以下操作:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"userA: \n friends: \n userBId: true \nuserB:\n friends:\n userAId: true"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"你通過兩個不同的路徑(userA\/friends\/userBId)和(userB\/friends\/userAId)對好友關係進行反正則化。要獲取完整數據,你需要手動複製一個聯接(join):"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"1. get `userA\/friends`\n2. for each id, get `\/${id}`"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這種關係在你的應用程序中很快就會出現。如果能有解決方案幫助你處理它就太好了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第二,權限。Firebase要求你使用一種受限的語言來編寫權限。在實踐中,這些規則很快就會變得非常混亂——於是人們開始自己編寫一些高級語言並編譯成Firebase規則。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們在Facebook對此進行了大量實驗,得出的結論是,你需要一種真正的語言來表達權限。如果Firebase有這樣的語言就會更加強大。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"至於剩下的項目(審計、撤消\/重做、寫入的離線模式、衍生數據)——Firebase還沒有解決它們。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"Supabase"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Supabase正在嘗試做Firebase爲Mongo所做的事情,但Supabase是爲Postgres做的。如果他們成功了,這將是一個非常有吸引力的選擇,因爲它將解決Firebase面臨的最大問題:查詢能力。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"到目前爲止,Supabase取得了一些重大進展。他們的身份驗證抽象非常棒,這讓它成爲少數幾個像firebase一樣容易上手的平臺之一。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"他們的實時選項允許你訂閱行級更新。例如,如果我們想知道一個好友是何時被創建、更新或更改的,我們可以這樣寫:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"const friendsChange = supabase\n .from('friendships:friend_one_id=eq.200')\n .on('*', handleFriendshipChange)\n .subscribe()"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在實踐中這可以讓你走得更遠。不過它可能會變得很麻煩。例如,如果我們創建了一個好友,我們可能沒有用戶信息,所以必須獲取它。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"function handleFriendshipChange(friendship) { \n if (!userStore.get(friendship.friend_two_id)) { \n fetchUser(...)\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏指出了Supabase的主要弱點:它還沒有“瀏覽器上的數據庫”這種抽象。雖然你可以做查詢,但你要自己負責正則化並處理數據。這意味着它不能自動進行樂觀更新,不能做響應式查詢等。他們的權限模型也很像Firebase,因爲它遵循了Postgres的行級安全性。一開始這是很好用的,但就像Firebase,它很快就會變得很麻煩。這些規則往往會拖慢查詢優化器的速度,並且SQL本身會變得越來越難推理。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"GraphQL+Hasura"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"GraphQL是一種很好的方法來聲明性地定義你想要從客戶端獲取的數據。像Hasura這樣的服務可以使用像Postgres這樣的數據庫,並做一些聰明的事情,比如給你一個GraphQL API。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Hasura很適合讀取數據。他們在處理聯接方面做得很聰明,並且可以給你一個很好的數據視圖。你可以用一個flip將任何查詢轉換爲訂閱。當我第一次嘗試將查詢轉換爲訂閱時,確實感覺這很神奇。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"今天GraphQL工具的一大問題是它們的原型製作速度。你往往需要多個不同的庫和構建步驟。他們在數據寫入方面做得也沒那麼好。樂觀更新不會自動發生——你必須自己處理它。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"小結"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們已經研究了三個最有前途的解決方案。現在,Firebase可以立刻解決大多數問題。Supabase以犧牲更多客戶端支持爲代價爲你提供了更好的查詢能力。Hasura以犧牲原型製作速度爲代價,爲你提供了更強大的訂閱和更強大的本地狀態。據我所知,還沒有方案能在客戶端解決衝突,提供撤消\/重做和強大的響應式查詢。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"未來"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現在的問題是:這些工具會演變成什麼樣子?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在某些層面,未來已經到來了。例如,我認爲Figma就是一款來自未來的應用:它可以出色地處理離線模式、撤消\/重做和多人關係。如果我們想製作這樣的應用,理想的數據抽象應該是什麼樣的?"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"需求"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"客戶端數據庫,有着強大的查詢語言"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從瀏覽器來看,這種抽象必須像firebase一樣,但要有強大的查詢語言。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"你應該能夠查詢本地數據,並且它應該與SQL一樣強大。你的查詢應該是響應式的,如果有更改會自動更新。它也應該爲你處理樂觀更新。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"user = useQuery(\"SELECT * FROM users WHERE id = ?\", 10);"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"真正的權限語言"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來,我們需要一種可組合的權限語言。FB的EntFramework也是我經常使用的例子,因爲它非常強大。我們應該能夠定義實體的規則,並且應該保證我們不會意外看到不允許我們看到的東西。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"User { \n view: [\n IAllowIfAdmin(),\n IAllowIfFriend(),\n IAllowIfSameUser(),\n ]\n write: [\n IAllowIfAdmin(),\n IAllowIfFriend(),\n ]\n}"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"離線模式和撤消\/重做"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最後,這個抽象應該讓我們更容易實現離線模式,或者撤消重做。如果發生本地寫入,並且服務器上存在寫入衝突,則應該有一個協調器在大多數情況下做出正確的決定。如果有問題,我們應該能夠朝着正確的方向推動它前進。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"無論我們選擇什麼抽象,它都應該讓我們能夠在離線時運行寫入操作。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"下一個雲"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最後,我們應該能夠表達數據依賴關係,而無需啓動任何東西。一個簡單的命令:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"db.user |> Redis"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對用戶的所有查詢都應該神奇地被Redis緩存。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"實現的草圖"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"好吧,這些需求聽起來很神奇。那麼今天滿足它們的實現會是什麼樣子?"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"Diatomic和Datascript"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在Clojure世界中,人們長期以來一直是Datomic的粉絲。Datomic是一個基於事實的數據庫,可以讓你“看到時間線上的每一個更改”。Nikita Tonsky還實現了datascript,這是一個與Datomic語義相同的客戶端數據庫和查詢引擎!"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"它們已被用於構建支持離線的應用程序(如Roam)或協作應用程序(如Precursor)。如果我們在後端打包一個類似Datomic的數據庫,在前端打包一個類似datascript的數據庫,它就可以成爲“具有強大查詢語言的客戶端數據庫”!"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"響應性"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Datomic讓你可以輕鬆地將新提交的事實訂閱到數據庫。如果我們在頂層創建一個服務,讓它保留查詢並聽取這些事實,是不是會很棒?出現一個更改後,我們將更新相關查詢。突然之間,我們的數據庫變成實時的了!"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"權限語言"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們的服務器可以接受一些代碼片段,並在獲取數據時運行它們。這些片段將負責處理權限,爲我們提供強大的權限語言!"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"管道"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最後,我們可以編寫一些DSL,讓你可以根據用戶的喜好將數據通過管道傳輸到Elastic Search、Redis等。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有了它,我們就有了一個優秀的方案。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"注意事項"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼,爲什麼這種方案還不存在呢?那是因爲……"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"Datalog還不流行"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果我們使用Datomic這樣的數據庫,我們就不會再使用SQL。Datomic使用一種基於邏輯的查詢語言,稱爲Datalog。現在它與SQL一樣強大,甚至更爲強大。唯一的問題是,對於外行來說,它看起來非常難上手的樣子:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"[:find [(pull ?c [:conversation\/user :conversation\/message]) ...]\n :where [?e :session\/thread ?thread-id] \n [?c :conversation\/thread ?thread-id]]"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個查詢將查找當前“會話”中活動線程的所有消息以及用戶信息。不錯!一旦你學會了它,就會意識到它是一種優雅而出色的語言。但我認爲這還不夠。原型製作速度需要非常快纔行,我們可能沒時間去學這種語言了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有一些有趣的實驗可以簡化這一過程。例如,Dennis Heihoff"},{"type":"link","attrs":{"href":"https:\/\/twitter.com\/denik\/status\/1290415892367540227","title":"","type":null},"content":[{"type":"text","text":"嘗試"}]},{"type":"text","text":"使用自然語言。這給我們啓發了一種有趣的解決方案:我們能否編寫一種稍微冗長但更加自然的查詢語言,把它編譯爲Datalog?我認同這種想法。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另一個問題是數據建模也與人們習慣的做法不一樣。Firebase是黃金標準,你可以在不指定任何schema的情況下編寫你的第一個更改。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"雖然做起來很難,但我認爲我們的目標應該是儘可能接近“簡單易用”。Datascript只要求你指明引用和多值屬性。Datomic需要一個schema,但也許如果我們使用開源的、基於datalog的數據庫,我們可以增強它來做類似的事情。要麼儘可能少用schema,要麼是“神奇的可檢測schema”。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"Datalog很難實現響應性"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SQL和Datalog都存在的一個大問題是,它們很難基於一些新的更改來確定哪些查詢需要更新。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我不認爲這是不可能解決的障礙。Hasura可以做輪詢,而且可擴展。我們也可以嘗試使用特定的訂閱語言,類似於Supabase。如果我們可以證明某些查詢只能通過事實的某些子集來更改,我們可以將它們從輪詢中移出。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這是一個棘手的問題,但我認爲它還是可以解決的。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"權限語言會減慢速度"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"讓權限檢查成爲一種成熟的語言的話,一個問題是我們容易過度獲取數據。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我認爲這個問題是值得考慮的,但如果使用像Datomic這樣的數據庫,我們就可以解決它。數據讀取很容易擴展和緩存。因爲一切都是事實,我們可以創建一個界面來引導人們只獲取他們需要的值。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Facebook就做到了這一點。這可能會很難,但終究是可行的。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"這個抽象可能太大了"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"框架通常無法通用化。例如,如果我們想共享鼠標位置怎麼辦?這是短暫的狀態,不適合數據庫,但我們確實需要讓它實時化——我們應該把它保存在哪裏?如果你構建這樣的抽象,將會出現很多這樣的事情,並且你很可能會搞錯。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我認爲這確實是一個問題。如果有人要解決這個問題,最好的辦法是採用Rails方法:使用它構建一個生產應用,並將內部組件提取爲產品。我認爲他們很有可能找到正確的抽象。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"它只會用於玩具項目"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這類產品的共同問題是,人們只會將它們用於業餘愛好項目,而且裏面不會有很多商機。我認爲Heroku和Firebase在這裏指明瞭正確的出路。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"大企業都是從業餘項目開始起家的。老一輩工程師可能將Firebase視爲玩具,但現在許多成功的初創公司都在使用Firebase。它不僅僅是一個數據庫,也許它還會成爲一個全新的平臺——甚至是AWS的繼任者。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"市場競爭非常激烈"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"市場競爭非常激烈,用戶變化無常。Slava的《"},{"type":"link","attrs":{"href":"https:\/\/www.defmacro.org\/2017\/01\/18\/why-rethinkdb-failed.html","title":"","type":null},"content":[{"type":"text","text":"爲什麼RethinkDB會失敗"}]},{"type":"text","text":"》描繪了在開發工具市場中獲勝的難度有多大。我不認爲他是錯的。這樣做需要對如何構建護城河並擴展成下一個AWS給出令人信服的回答。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"結語"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"好吧,我們涵蓋了痛點,討論了競爭對手,介紹了理想的解決方案,並考慮了諸多問題。謝謝你陪我走過這段旅程!"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文鏈接:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"https:\/\/stopa.io\/post\/279"}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章