文章系列
- 為什麼你應該關心?
- 可能出現什麼問題?
- 採用新技術的障礙是什麼?
- 新算法如何提供幫助?
上一篇文章解釋了強一致性(與最終一致性相對)。本文是系列文章的第二部分,我們將解釋缺乏強一致性如何使提供良好的最終用戶體驗變得更加困難,如何帶來嚴重的工程開銷,以及如何使您容易受到攻擊。本部分較長,因為我們將解釋不同的數據庫異常,並通過幾個示例場景,並簡要介紹哪種類型的數據庫容易受到每種異常的影響。
用戶體驗是任何應用程序成功的驅動因素,依賴於不一致的後端會增加提供良好體驗的挑戰。更重要的是,在不一致的數據之上構建應用程序邏輯會導致漏洞。一篇論文將這類攻擊稱為“ACIDrain”。他們調查了12 個最流行的自託管電子商務應用程序,並發現了至少22 種可能的嚴重攻擊。一個網站是比特幣錢包服務,由於這些攻擊不得不關閉。當您選擇一個不是100% ACID 的分佈式數據庫時,就會有麻煩。正如我們之前的例子所解釋的那樣,由於誤解、定義不明確的術語和激進的營銷,工程師很難確定特定數據庫提供的保證。
什麼麻煩?您的應用程序可能會出現諸如帳戶餘額錯誤、用戶獎勵未收到、交易執行兩次、消息亂序或違反應用程序規則等問題。有關為什麼需要分佈式數據庫以及為什麼它們很困難的快速介紹,請參閱我們的第一篇文章或此精彩的視頻解釋。簡而言之,分佈式數據庫是一個將數據的副本保存在多個位置的數據庫,原因是可擴展性、延遲和可用性。
我們將介紹這四個潛在問題(還有更多問題),並用遊戲開發中的例子來說明它們。遊戲開發很複雜,這些開發人員面臨許多與現實生活中的嚴重問題非常相似的問題。遊戲有交易系統、消息系統、需要滿足條件的獎勵等等。記住,如果事情出錯或看起來出錯,遊戲玩家會多么生氣(或高興?)。在遊戲中,用戶體驗至關重要,因此遊戲開發人員通常承受著巨大的壓力,以確保他們的系統具有容錯性。
準備好了嗎?讓我們深入探討第一個潛在問題!
1. 陳舊讀取
陳舊讀取是指返回舊數據的讀取,換句話說,返回的值根據最新的寫入尚未更新。許多分佈式數據庫,包括使用副本進行向上擴展的傳統數據庫(閱讀第1 部分以了解這些數據庫的工作原理),都會出現陳舊讀取問題。
對最終用戶的影響
首先,陳舊讀取會影響最終用戶。而且這不是單一的影響。
令人沮喪的體驗和不公平的優勢
想像一下,遊戲中兩個用戶遇到一個裝有金幣的寶箱。第一個用戶從一個數據庫服務器接收數據,而第二個用戶連接到第二個數據庫服務器。事件順序如下:
- 用戶1(通過數據庫服務器1)看到並打開寶箱,取走金幣。
- 用戶2(通過數據庫服務器2)看到一個裝滿的寶箱,打開它,失敗了。
- 用戶2 仍然看到一個裝滿的寶箱,並且不明白為什麼它會失敗。
雖然這似乎是一個小問題,但結果是第二個玩家的體驗令人沮喪。他不僅處於劣勢,而且還經常會在遊戲中看到一些看起來存在但實際上不存在的情況。接下來,讓我們來看一個玩家對陳舊讀取採取行動的例子!
陳舊讀取導致重複寫入
想像一下,遊戲中一個角色試圖在商店裡購買盾牌和劍的情況。如果有多個位置包含數據並且沒有智能係統來提供一致性,則一個節點包含的數據將比另一個節點舊。在這種情況下,用戶可能會購買物品(聯繫第一個節點),然後檢查他的庫存(聯繫第二個節點),卻發現物品不存在。用戶可能會感到困惑,並可能認為交易沒有成功。在這種情況下,大多數人會怎麼做?好吧,他們會再次嘗試購買該商品。一旦第二個節點趕上,用戶已經購買了副本,一旦副本趕上,他突然發現他的錢用完了,每種物品都有兩件。他認為我們的遊戲壞了。
在這種情況下,用戶花費了他不希望花費的資源。如果我們在這樣的數據庫之上編寫電子郵件客戶端,用戶可能會嘗試發送電子郵件,然後刷新瀏覽器,但無法檢索他剛剛發送的電子郵件,因此會再次發送。在這樣的系統之上提供良好的用戶體驗和實現安全的交易(例如銀行交易)非常困難。
對開發人員的影響
在編碼時,您總是必須預期某些東西(尚未)存在並相應地進行編碼。當讀取最終一致時,編寫防故障代碼變得非常具有挑戰性,並且用戶很可能會在您的應用程序中遇到問題。當讀取最終一致時,這些問題會在您能夠調查它們之前消失。基本上,您最終是在追逐幽靈。開發人員仍然經常選擇最終一致的數據庫或分發方法,因為通常需要時間才能注意到問題。然後,一旦他們的應用程序中出現問題,他們就會嘗試發揮創意並在其傳統數據庫之上構建解決方案(1, 2) 來修復陳舊讀取。存在許多這樣的指南以及Cassandra 等數據庫已經實現了一些一致性功能的事實表明,這些問題是真實的,並且比您想像的更頻繁地在生產系統中造成問題。在非為一致性而構建的系統之上構建的自定義解決方案非常複雜且脆弱。如果存在開箱即用地提供強一致性的數據庫,為什麼有人會經歷這樣的麻煩呢?
表現出這種異常的數據庫
使用主讀取複製的傳統數據庫(PostgreSQL、MySQL、SQL Server 等)通常會出現陳舊讀取問題。許多較新的分佈式數據庫也最初是最終一致的,或者換句話說,沒有針對陳舊讀取的保護。這是由於開發人員社區中的一種強烈信念,即這是擴展所必需的。最著名的數據庫就是這樣開始的,但它認識到其用戶如何努力處理這種異常,並且此後提供了額外的措施來避免這種情況。較舊的數據庫或未設計為高效提供強一致性的數據庫(如Cassandra、CouchDB 和DynamoDB)默認情況下最終是一致的。 Riak 等其他方法最終也是一致的,但通過實現衝突解決系統來降低過時值的機率,採取了不同的路徑。但是,這並不能保證您的數據安全,因為衝突解決並非萬無一失。
2. 丟失寫入
在分佈式數據庫領域,當寫入同時發生時,需要做出一個重要的選擇。一種選擇(安全的選擇)是確保所有數據庫節點都能就這些寫入的順序達成一致。這遠非微不足道,因為它要么需要同步時鐘(為此需要特定的硬件),要么需要像Calvin 這樣的不依賴於時鐘的智能算法。第二個不太安全的選擇是允許每個節點本地寫入,然後稍後決定如何處理衝突。選擇第二種選擇的數據庫可能會丟失您的寫入。
對最終用戶的影響
考慮遊戲中兩個交易,我們從11 個金幣開始併購買兩件物品。首先,我們以5 個金幣的價格購買一把劍,然後以5 個金幣的價格購買一個盾牌,並且這兩個交易都定向到我們分佈式數據庫的不同節點。每個節點讀取該值,在本例中,對於兩個節點來說仍然是11。兩個節點都將決定寫入6 作為結果(11-5),因為它們不知道任何復制。由於第二個事務尚未看到第一個寫入的值,因此玩家最終以總共5 個金幣的價格購買了劍和盾牌,而不是10 個金幣。對用戶有利,但對系統不利!為了補救這種行為,分佈式數據庫有幾種策略——有些比其他策略更好。
解決策略包括“最後寫入獲勝”(LWW)或“最長版本歷史記錄”(LVH)獲勝。長期以來,LWW 一直是Cassandra 的策略,如果您不進行不同的配置,它仍然是默認行為。
如果我們將LWW 衝突解決應用於我們之前的示例,玩家仍然剩下6 個金幣,但只會購買一件物品。這是一種糟糕的用戶體驗,因為應用程序確認了他對第二件物品的購買,即使數據庫沒有將其識別為存在於他的庫存中。
不可預測的安全性
正如您可能想像的那樣,在這樣的系統之上編寫安全規則是不安全的。許多應用程序依賴於後端(或盡可能直接在數據庫上)的複雜安全規則來確定用戶是否可以訪問資源。當這些規則基於不可靠地更新的陳舊數據時,我們如何才能確保永遠不會發生違規?想像一下,PaaS 應用程序的一位用戶打電話給他的管理員並詢問:“您能否將此公共組設為私有,以便我們可以將其重新用於內部數據?”管理員應用該操作並告訴他已完成。但是,由於管理員和用戶可能位於不同的節點上,因此用戶可能會開始將敏感數據添加到技術上仍然是公共的組中。
對開發人員的影響
當寫入丟失時,調試用戶問題將是一場噩夢。想像一下,用戶報告說他在您的應用程序中丟失了數據,然後過了一天才有時間回复。您將如何嘗試找出問題是由您的數據庫還是由有故障的應用程序邏輯引起的?在允許跟踪數據歷史記錄的數據庫(如FaunaDB 或Datomic)中,您可以追溯到過去以查看數據是如何被操作的。但是,這些數據庫都不會受到丟失寫入的影響,並且確實容易受到這種異常影響的數據庫通常不具備時光倒流功能。
容易丟失寫入的數據庫
所有使用衝突解決而不是衝突避免的數據庫都會丟失寫入。 Cassandra 和DynamoDB 使用最後寫入獲勝(LWW) 作為默認值;MongoDB 曾經使用LWW,但此後已放棄使用。傳統數據庫(如MySQL)中的主-主分發方法提供不同的衝突解決策略。許多未為一致性而構建的分佈式數據庫都會丟失寫入。 Riak 最簡單的衝突解決是由LWW 驅動的,但它們也實現了更智能的系統。但即使使用智能係統,有時也沒有明顯的方法來解決衝突。 Riak 和CouchDB 將選擇正確寫入的責任交給客戶端或應用程序,允許他們手動選擇要保留哪個版本。
由於分發很複雜,大多數數據庫都使用不完善的算法,因此當節點崩潰或出現網絡分區時,許多數據庫中都會經常發生丟失寫入。即使是MongoDB(它不分發寫入(寫入轉到一個節點)),在節點在寫入後立即宕機的罕見情況下也可能發生寫入衝突。
3. 寫入偏差
寫入偏差是數據庫供應商稱之為快照一致性的保證類型中可能發生的事情。在快照一致性中,事務從事務開始時拍攝的快照中讀取。快照一致性可以防止許多異常。事實上,許多人認為它是完全安全的,直到出現論文(PDF) 證明事實並非如此。因此,開發人員難以理解為什麼某些保證不夠好也就不足為奇了。
在我們討論快照一致性中哪些不起作用之前,讓我們首先討論哪些起作用。假設我們有一個騎士和法師之間的戰鬥,他們各自的生命力由四個心組成。
當任何角色受到攻擊時,事務是一個計算已移除多少心的函數:
<code>damageCharacter(character, damage) { character.hearts = character.hearts - damage character.dead = isCharacterDead(character) }</code>
並且,在每次攻擊之後,另一個isCharacterDead 函數也會運行以查看角色是否還有任何心:
<code>isCharacterDead(character) { if (character.hearts </code>
在一個簡單的情況下,騎士的攻擊從法師身上移除了三個心,然後法師的法術從騎士身上移除了四個心,使他自己的生命值恢復到四個。如果一個事務在另一個事務之後運行,那麼這兩個事務在大多數數據庫中的行為將是正確的。
但是,如果我們添加第三個事務,即騎士的攻擊,它與法師的法術同時運行呢?
騎士死了嗎,法師還活著嗎?
為了處理這種混亂,快照一致性系統通常實現一個稱為“第一個提交者獲勝”的規則。只有當另一個事務尚未寫入同一行時,事務才能結束,否則它將回滾。在這個例子中,由於兩個事務都試圖寫入同一行(法師的健康值),只有生命吸取法術會起作用,而騎士的第二次攻擊將被回滾。然後,最終結果將與之前的示例相同:一個死去的騎士和一個擁有完整生命值的法師。
但是,MySQL 和InnoDB 等一些數據庫並不認為“第一個提交者獲勝”是快照隔離的一部分。在這種情況下,我們將有一個丟失寫入:法師現在死了,儘管他應該在騎士的攻擊生效之前從生命吸取中獲得生命值。 (我們確實提到了定義不明確的術語和寬鬆的解釋,對吧?)
包含“第一個提交者獲勝”規則的快照一致性確實很好地處理了一些事情,這並不奇怪,因為它長期以來被認為是一個很好的解決方案。這仍然是PostgreSQL、Oracle 和SQL Server 的方法,但它們都有不同的名稱。 PostgreSQL 將此保證稱為“可重複讀取”,Oracle 將其稱為“可序列化”(根據我們的定義是不正確的),而SQL Server 將其稱為“快照隔離”。難怪人們在這個術語森林中迷路了。讓我們看看它沒有按預期運行的示例!
對最終用戶的影響
接下來的戰鬥將發生在兩支軍隊之間,如果所有軍隊的角色都死了,則一支軍隊被認為是死了的:
isArmyDead(army){ if (all characters are dead) { return true } else { return false } }
在每次攻擊之後,以下函數確定角色是否死亡,然後運行上述函數以查看軍隊是否死亡:
damageArmyCharacter(army, character, damage){ character.hearts = character.hearts - damage character.dead = isCharacterDead(character) armyDead = isArmyDead(army) if (army.dead != armyDead){ army.dead = armyDead } }
首先,角色的心臟會因受到的傷害而減少。然後,我們通過檢查每個角色是否沒有生命值來驗證軍隊是否死亡。然後,如果軍隊的狀態發生了變化,我們將更新軍隊的“死亡”布爾值。
有三個法師分別攻擊一次,導致三個“生命吸取”事務。快照是在事務開始時拍攝的,由於所有事務同時開始,因此快照是相同的。每個事務都有一個數據副本,其中所有騎士仍然擁有完整的生命值。
讓我們看看第一個“生命吸取”事務是如何解決的。在這個事務中,mage1 攻擊knight1,騎士損失4 個生命值,而攻擊法師恢復了全部生命值。事務決定騎士軍隊沒有死亡,因為它只能看到一個快照,其中兩個騎士仍然擁有完整的生命值,一個騎士死了。其他兩個事務作用於另一個法師和騎士,但以類似的方式進行。這些事務中的每一個最初在其數據副本中都有三個活著的騎士,並且只看到一個騎士死亡。因此,每個事務都決定騎士軍隊仍然活著。
當所有事務完成後,沒有一個騎士還活著,但我們指示軍隊是否死亡的布爾值仍然設置為false。為什麼?因為在拍攝快照時,沒有一個騎士死亡。因此,每個事務都看到自己的騎士死亡,但不知道軍隊中的其他騎士。雖然這是我們系統中的異常(稱為寫入偏差),但寫入已通過,因為它們各自寫入不同的角色,並且對軍隊的寫入從未改變。太棒了,我們現在有一支幽靈軍隊!
對開發人員的影響
數據質量
如果我們想確保用戶擁有唯一的名稱怎麼辦?我們創建用戶的事務將檢查名稱是否存在;如果不存在,我們將使用該名稱寫入新用戶。但是,如果兩個用戶嘗試使用相同的名稱註冊,快照將不會注意到任何事情,因為用戶被寫入不同的行,因此不會發生衝突。我們現在系統中有兩個同名用戶。
由於寫入偏差,可能會出現許多其他異常示例。如果您感興趣,Martin Kleppman 的著作《設計數據密集型應用程序》對此進行了更詳細的描述。
以不同的方式編寫代碼以避免回滾
現在,讓我們考慮一種不同的方法,其中攻擊並非針對軍隊中的特定角色。在這種情況下,數據庫負責選擇首先應該攻擊哪個騎士。
damageArmy(army, damage){ character = getFirstHealthyCharacter(knight) character.hearts = character.hearts - damage character.dead = isCharacterDead(character) // ... }
如果我們像之前的示例一樣並行執行多次攻擊,則getFirstHealthyCharacter 將始終針對相同的騎士,這將導致多個事務寫入同一行。這將被“第一個提交者獲勝”規則阻止,該規則將回滾其他兩次攻擊。雖然它可以防止異常,但開發人員需要了解這些問題並圍繞它們創造性地編寫代碼。但是,如果數據庫開箱即用地為您做到這一點,豈不是更容易嗎?
容易出現寫入偏差的數據庫
任何提供快照隔離而不是可序列化性的數據庫都可能出現寫入偏差。有關數據庫及其隔離級別的概述,請參閱本文。
4. 亂序寫入
為了避免丟失寫入和陳舊讀取,分佈式數據庫的目標是所謂的“強一致性”。我們提到數據庫可以選擇就全局順序達成一致(安全的選擇)或決定解決衝突(導致丟失寫入的選擇)。如果我們決定一個全局順序,這意味著儘管劍和盾牌是並行購買的,但最終結果應該表現得好像我們先買了劍,然后買了盾牌一樣。這通常也稱為“線性化”,因為您可以線性化數據庫操作。線性化是確保數據安全的黃金標準。
不同的供應商提供不同的隔離級別,您可以在此處進行比較。經常出現的一個術語是可序列化性,它是強一致性(或線性化)的略微不太嚴格的版本。可序列化性已經相當強,並且涵蓋了大多數異常,但仍然為由於寫入被重新排序而導致的一種非常細微的異常留下了空間。在這種情況下,即使事務已提交,數據庫也可以自由地切換該順序。簡單來說,線性化是可序列化性加上保證的順序。當數據庫缺少此保證的順序時,您的應用程序容易受到亂序寫入的影響。
對最終用戶的影響
對話重新排序
如果有人因錯誤而發送第二條消息,則可以以令人困惑的方式對對話進行排序。
用戶操作重新排序
如果我們的玩家有11 個金幣,並且只是按重要性順序購買物品,而沒有主動檢查他擁有的金幣數量,那麼數據庫可以重新排序這些購買訂單。如果他沒有足夠的錢,他本可以先購買最重要性最低的物品。
在這種情況下,有一個數據庫檢查驗證我們是否有足夠的黃金。想像一下,我們沒有足夠的錢,讓賬戶低於零會花費我們錢,就像銀行在您低於零時向您收取透支費一樣。您可能會快速出售一件物品以確保您有足夠的錢購買所有三件物品。但是,旨在增加您餘額的銷售可能會重新排序到交易列表的末尾,這將有效地將您的餘額推至零以下。如果這是一家銀行,您很可能會產生您絕對不應承擔的費用。
不可預測的安全性
在配置安全設置後,用戶會期望這些設置將應用於所有即將執行的操作,但是當用戶通過不同的渠道相互交談時,可能會出現問題。記住我們討論過的例子,管理員正在與想要將一個組設為私有的用戶通話,然後向其中添加敏感數據。雖然在提供可序列化性的數據庫中,這種情況可能發生的時間窗口變得更小,但這仍然可能發生,因為管理員的操作可能直到用戶操作之後才能完成。當用戶通過不同的渠道進行溝通並期望數據庫實時排序時,事情就會出錯。
如果由於負載平衡而將用戶重定向到不同的節點,則也會發生此異常。在這種情況下,兩個連續的操作最終位於不同的節點上,並且可能會重新排序。如果一個女孩將她的父母添加到一個查看權限有限的facebook 群組中,然後發布她的春假照片,那麼這些圖片仍然可能會出現在她父母的feed 中。
在另一個例子中,自動交易機器人可能具有設置,例如最高購買價格、支出限額和要關注的股票列表。如果用戶更改機器人應該購買的股票列表,然後更改支出限額,那麼如果這些交易被重新排序並且交易機器人已將新分配的預算用於舊股票,他將不會感到高興。
對開發人員的影響
漏洞
一些漏洞取決於事務的潛在反轉。想像一下,遊戲玩家一旦擁有1,000 個金幣就會獲得獎杯,而他非常想要這個獎杯。遊戲通過將多個容器的金幣加在一起(例如他的存儲和他的攜帶物(他的庫存))來計算玩家有多少金幣。如果玩家在存儲和庫存之間快速交換金錢,他實際上可以欺騙系統。
在下圖中,第二個玩家充當犯罪夥伴,以確保存儲和庫存之間的資金轉移發生在不同的交易中,從而增加這些交易被路由到不同節點的機會。現實世界中更嚴重的例子是使用第三方賬戶轉賬的銀行;銀行可能會錯誤計算某人是否有資格獲得貸款,因為各種交易已被發送到不同的節點並且沒有足夠的時間進行排序。
容易出現亂序寫入的數據庫
任何不提供線性化的數據庫都可能出現寫入偏差。有關哪些數據庫提供線性化的概述,請參閱本文。劇透:並不多。
所有異常都可能在一致性受限時返回
最後要討論的強一致性的一個放鬆是僅保證在一定的範圍內。典型的範圍是數據中心區域、分區、節點、集合或行。如果您在對強一致性施加這些類型邊界的數據庫之上進行編程,那麼您需要記住這些邊界,以避免意外地再次打開潘多拉魔盒。
下面是一個一致性的示例,但僅保證在一個集合內。下面的示例包含三個集合:一個用於玩家,一個用於鐵匠(即修理玩家物品的黑鐵匠),另一個用於物品。每個玩家和每個鐵匠都有一個指向物品集合中物品的id 列表。
如果您想在兩個玩家之間交易盾牌(例如,從Brecht 到Robert),那麼一切都會很好,因為您仍然在一個集合中,因此您的事務仍然在保證一致性的範圍內。但是,如果羅伯特的劍在鐵匠那裡進行修理,並且他想取回它怎麼辦?然後,事務跨越兩個集合,即鐵匠的集合和玩家的集合,並且保證被取消。這種限制通常存在於MongoDB 等文檔數據庫中。然後,您需要更改編程方式以找到圍繞限制的創造性解決方案。例如,您可以將物品的位置編碼到物品本身。
當然,真正的遊戲很複雜。您可能希望能夠將物品掉落在地板上或將它們放置在市場上,以便物品可以由玩家擁有,但不一定在玩家的庫存中。當事情變得更複雜時,這些變通方法會顯著增加技術深度並改變您的編碼方式以保持在數據庫的保證範圍內。
結論
我們已經看到了當您的數據庫行為與預期不符時可能出現的不同問題的示例。雖然有些情況乍一看似乎微不足道,但它們都會對開發人員的生產力產生重大影響,尤其是在系統擴展時。更重要的是,它們使您容易受到不可預測的安全漏洞的攻擊——這可能會對您應用程序的聲譽造成不可挽回的損害。
我們討論了幾種一致性程度,但是現在我們已經看到了這些示例,讓我們將它們放在一起:
還請記住,這些正確性保證中的每一個都可能帶有邊界:
最後,請意識到我們只提到了少數異常和一致性保證,而實際上還有更多。對於感興趣的讀者,我強烈推薦Martin Kleppman 的《設計數據密集型應用程序》。
我們生活在一個我們不再需要關心的時代,只要我們選擇一個沒有限制的強一致性數據庫即可。由於Calvin(FaunaDB)和Spanner(Google Spanner、FoundationDB)等新方法的出現,我們現在擁有多區域分佈式數據庫,這些數據庫可以提供極低的延遲,並且在每種情況下都按預期運行。那麼,為什麼您還要冒著搬起石頭砸自己的腳的風險,選擇一個不提供這些保證的數據庫呢?
在本系列的下一篇文章中,我們將介紹對開發人員體驗的影響。為什麼很難讓開發人員相信一致性很重要?劇透:大多數人需要親身體驗才能看到必要性。但是請考慮一下:“如果出現錯誤,是您的應用程序錯誤,還是數據錯誤?您如何知道?”一旦數據庫的限製表現為錯誤或糟糕的用戶體驗,您就需要解決數據庫的限制,這會導致效率低下的粘合代碼,這些代碼無法擴展。當然,在那時,您已經投入了大量資金,並且意識到得太晚了。
文章系列
- 為什麼你應該關心?
- 可能出現什麼問題?
- 採用新技術的障礙是什麼?
- 新算法如何提供幫助?
以上是一致的後端和UX:怎麼了?的詳細內容。更多資訊請關注PHP中文網其他相關文章!

在這篇文章中,布萊克·莫里(Blackle Mori)向您展示了一些駭客,同時試圖推動同位HTML支持的極限。如果您敢於使用這些,以免您也被標記為CSS罪犯。

具有CSS的自定義光標很棒,但是我們可以將JavaScript提升到一個新的水平。使用JavaScript,我們可以在光標狀態之間過渡,將動態文本放置在光標中,應用複雜的動畫並應用過濾器。

互動CSS動畫和元素相互啟動的元素在2025年似乎更合理。雖然不需要在CSS中實施乒乓球,但CSS的靈活性和力量的增加,可以懷疑Lee&Aver Lee有一天會成為一種

有關利用CSS背景濾波器屬性來樣式用戶界面的提示和技巧。您將學習如何在多個元素之間進行背景過濾器,並將它們與其他CSS圖形效果集成在一起以創建精心設計的設計。

好吧,事實證明,SVG的內置動畫功能從未按計劃進行棄用。當然,CSS和JavaScript具有承載負載的能力,但是很高興知道Smil並沒有像以前那樣死在水中

是的,讓#039;跳上文字包裝:Safari Technology Preview In Pretty Landing!但是請注意,它與在鉻瀏覽器中的工作方式不同。

此CSS-tricks更新了,重點介紹了年鑑,最近的播客出現,新的CSS計數器指南以及增加了幾位新作者,這些新作者貢獻了有價值的內容。

在大多數情況下,人們展示了@Apply的@Apply功能,其中包括Tailwind的單個property實用程序之一(會改變單個CSS聲明)。當以這種方式展示時,@Apply聽起來似乎很有希望。如此明顯


熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

Video Face Swap
使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

熱工具

Dreamweaver CS6
視覺化網頁開發工具

SublimeText3 英文版
推薦:為Win版本,支援程式碼提示!

mPDF
mPDF是一個PHP庫,可以從UTF-8編碼的HTML產生PDF檔案。原作者Ian Back編寫mPDF以從他的網站上「即時」輸出PDF文件,並處理不同的語言。與原始腳本如HTML2FPDF相比,它的速度較慢,並且在使用Unicode字體時產生的檔案較大,但支援CSS樣式等,並進行了大量增強。支援幾乎所有語言,包括RTL(阿拉伯語和希伯來語)和CJK(中日韓)。支援嵌套的區塊級元素(如P、DIV),

EditPlus 中文破解版
體積小,語法高亮,不支援程式碼提示功能

SAP NetWeaver Server Adapter for Eclipse
將Eclipse與SAP NetWeaver應用伺服器整合。