前段時間在 github 上看到了一個很「trick」的項目:用純 CSS(即不使用 JavaScript)實現一個聊天應用 —— css-only-chat。即下圖所示效果。css
在咱們的印象裏,實現一個簡單的聊天應用(消息發送與多頁面同步)並不困難 —— 這是在咱們有 JavaScript 的幫助下。而若是讓你只能使用 CSS,不能有前端的 JavaScript 代碼,那你可以實現麼?html
原版是用 Ruby 寫的後端。可能你們對 Ruby 不太瞭解,因此我按照原做者思路,用 NodeJS 實現了一版 css-only-chat-node,對你們來講可能會更易讀些。前端
首先強調一下,服務端的代碼確定仍是須要寫的,並且這部分顯然不能是 CSS。因此這裏的「純 CSS」主要指在瀏覽器端只使用 CSS。node
回憶一下,若是使用 JavaScript 來實現上圖中展現的聊天功能,有哪些問題須要處理呢?git
click
事件監聽,包括字符按鈕的點擊與發送按鈕的點擊;涉及到 JavaScript 的操做主要就是上面四個了。可是,如今咱們只能使用 CSS,那對於上面這幾個操做,能夠用什麼方式實現呢?github
使用 JavaScript 的話一行代碼能夠搞定:後端
document.getElementById('btn').addEventListener('click', function () {
// ……
});
複製代碼
使用 CSS 的話,其實有個僞類能夠幫咱們,即:active
。它能夠選擇激活的元素,而當咱們點擊某個元素時,它就會處於激活狀態。瀏覽器
因此,對於上面動圖中的26個字母(再加上 send 按鈕),能夠分配不一樣的classname
,而後設置僞類選擇器,這樣就能夠在點擊該字母對應的按鈕時觸發命中某個 CSS 規則。例如能夠對字符「a」設置以下規則用於「捕獲」點擊:服務器
.btn_a:active {
/* …… */
}
複製代碼
若是有 JavaScript 的幫助,發送請求只須要用個 XHR 便可,很方便。而對於 CSS,若是要想發一個請求的話有什麼辦法麼?app
可使用background-image
屬性,將它指定爲某個 URL,這樣前端就會向服務器發起一個背景圖片的請求。之因此可使用background-image
屬性還由於:瀏覽器只有在該 CSS 選擇器規則被實際應用到 DOM 元素後纔會實際發起background-image
的請求。例以下面這個規則:
.btn_a:active {
background-image: url('/keys/a');
}
複製代碼
只有在字符「a」被點擊後,瀏覽器纔會向服務器請求/keys/a
這張「圖片」。而在服務器端,經過判斷 URL 能夠知道前端點擊了哪一個字符。例如,對於按鈕「b」會有以下規則:
.btn_b:active {
background-image: url('/keys/b');
}
複製代碼
這樣就至關於實現了在 URL(/keys/a
與/keys/b
) 中「傳參」。
實時的消息展現,核心會用到一種叫「服務器推」的技術。其中比較常見方式有:
但這些方法都沒法規避 JavaScript,顯然不符合我們的要求。其實還有一種方式,我在《各種「服務器推」技術原理與實例》中也有提到,那就是基於 iframe 的長鏈接流(stream)模式。
這裏咱們主要是借鑑了「長鏈接流」這種模式。讓咱們的頁面永遠處於一個未加載完成的狀態。可是,因爲請求頭中包含Transfer-Encoding: chunked
,它會告訴瀏覽器,雖然頁面沒有返回結束,但你能夠開始渲染頁面了。正是因爲該請求的響應永遠不會結束,因此咱們能夠不斷向其中寫入新的內容,來更新頁面展現。
實現起來也很是簡單。http.ServerResponse
類自己就是繼承自Stream
的,因此只要在須要更新頁面內容時調用.write()
方法便可。例以下面這段代碼,能夠每隔2s在頁面上動態添加 "hello" 字符串而不須要任何瀏覽器端的配合(也就不須要寫 JavaScript 代碼了):
const http = require('http');
http.createServer((req, res) => {
res.setHeader('connection', 'keep-alive');
res.setHeader('content-type', 'text/html; charset=utf-8');
res.statusCode = 200;
res.write('I will update by myself');
setInterval(() => res.write('<br>hello'), 2000);
}).listen(8085);
複製代碼
在上一節咱們已經能夠經過 Stream 的方式,不借助 JavaScript 便可動態改變頁面內容了。可是若是你細心會發現,這種方式只能不斷「append」內容。而在咱們的例子中,看起來更像是可以動態改變某個 DOM 中的文本,例如隨着點擊不一樣按鈕,「Current Message」後面的文本會不斷變化。
這裏其實也有個很「trick」的方式。下圖這個部分(咱們姑且叫它 ChatPanel 吧)
其實咱們每次調用res.write()
時都會返回一個全新的 ChatPanel 的 HTML 片斷。於此同時,還會附帶一個<style>
元素,將以前的 ChatPanel 設爲display: none
。因此看起來像是更新了原來的 ChatPanel 的內容,但實際上是 append 了一個新的,同時隱藏以前的 ChatPanel。
到目前爲止,基本的方案都有了,但還有一個重要的問題:
在 CSS 規則中的background-image
只會在第一次應用到元素時發起請求,以後就不會再向服務器請求了。也就是說,用
.btn_a:active {
background-image: url('/keys/a');
}
複製代碼
這種規則,「a」 這個按鈕點過一次以後,下次再點擊就毫無反應了 —— 即後端收不到請求了。
要解決這個問題有一個方法。能夠在每次返回的新的 ChatPanel(ChatPanel 是啥我們在上一節中提到了,若是忘了能夠回去看下)裏,爲每一個字符按鈕都應用一套新的樣式規則,並設置新的背景圖 URL。例如咱們第一次點擊了「h」以後,返回的 ChatPanel 裏的按鈕「a」的classname
會該成btn_h_a
,對應的 CSS 規則改成:
.btn_h_a:active {
background-image: url('/keys/h_a');
}
複製代碼
再次點擊「i」以後,ChatPanel 裏對應的按鈕的樣式規則改成:
.btn_hi_a:active {
background-image: url('/keys/hi_a');
}
複製代碼
爲了可以保存未發送的內容(點擊 send 按鈕以前的輸入內容),以及同步歷史消息,須要有個地方存儲用戶輸入。同時咱們還會爲每一個鏈接設定一個惟一的用戶 ID。在原版的 css-only-chat 中使用了 Redis。我在 css-only-chat-node 中爲了簡便,直接存儲在了運行時的內存變量中了。
也許有朋友會問,這個 DEMO 有什麼實用價值麼?能夠發展成一個可用的聊天工具麼?
好吧,其實我以爲沒有太大用。可是裏面涉及到的一些「知識點」到是瞭解下也無妨。咱們天天面對那麼多無趣的需求,偶爾看看這種「有意思」的項目也算是放鬆一下吧。
最後,若是想看具體的運行效果,或者想了解代碼的細節,能夠看這裏:
Just have fun! 😜