如何在零JS代碼狀況下實現一個實時聊天功能❓

引言

前段時間在 github 上看到了一個很「trick」的項目:用純 CSS(即不使用 JavaScript)實現一個聊天應用 —— css-only-chat。即下圖所示效果。css

在咱們的印象裏,實現一個簡單的聊天應用(消息發送與多頁面同步)並不困難 —— 這是在咱們有 JavaScript 的幫助下。而若是讓你只能使用 CSS,不能有前端的 JavaScript 代碼,那你可以實現麼?html

原版是用 Ruby 寫的後端。可能你們對 Ruby 不太瞭解,因此我按照原做者思路,用 NodeJS 實現了一版 css-only-chat-node,對你們來講可能會更易讀些。前端

1. 咱們要解決什麼問題

首先強調一下,服務端的代碼確定仍是須要寫的,並且這部分顯然不能是 CSS。因此這裏的「純 CSS」主要指在瀏覽器端只使用 CSS。node

回憶一下,若是使用 JavaScript 來實現上圖中展現的聊天功能,有哪些問題須要處理呢?git

  • 首先,須要添加按鈕的click事件監聽,包括字符按鈕的點擊與發送按鈕的點擊;
  • 其次,點擊相應按鈕後,要將信息經過 Ajax 的方式發送到後端服務;
  • 再者,要實現實時的消息展現,通常會創建一個 WebSocket 鏈接;
  • 最後,對於後端同步來的消息,咱們會在瀏覽器端操做 DOM API 來改變 DOM 內容,展現消息記錄。

涉及到 JavaScript 的操做主要就是上面四個了。可是,如今咱們只能使用 CSS,那對於上面這幾個操做,能夠用什麼方式實現呢?github

2. Trick Time

2.1. 解決「點擊監聽」的問題

使用 JavaScript 的話一行代碼能夠搞定:後端

document.getElementById('btn').addEventListener('click', function () {
    // ……
});
複製代碼

使用 CSS 的話,其實有個僞類能夠幫咱們,即:active。它能夠選擇激活的元素,而當咱們點擊某個元素時,它就會處於激活狀態。瀏覽器

因此,對於上面動圖中的26個字母(再加上 send 按鈕),能夠分配不一樣的classname,而後設置僞類選擇器,這樣就能夠在點擊該字母對應的按鈕時觸發命中某個 CSS 規則。例如能夠對字符「a」設置以下規則用於「捕獲」點擊:服務器

.btn_a:active {
    /* …… */ 
}
複製代碼

2.2. 發送請求

若是有 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) 中「傳參」。

2.3. 實時消息展現

實時的消息展現,核心會用到一種叫「服務器推」的技術。其中比較常見方式有:

  • 使用 JavaScript 來和服務端創建 WebSocket 鏈接
  • 使用 JavaScript 建立定時器,定時發送請求輪詢
  • 使用 JavaScript 和服務端配合來實現長輪詢

但這些方法都沒法規避 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);
複製代碼

2.4. 改變頁面信息

在上一節咱們已經能夠經過 Stream 的方式,不借助 JavaScript 便可動態改變頁面內容了。可是若是你細心會發現,這種方式只能不斷「append」內容。而在咱們的例子中,看起來更像是可以動態改變某個 DOM 中的文本,例如隨着點擊不一樣按鈕,「Current Message」後面的文本會不斷變化。

這裏其實也有個很「trick」的方式。下圖這個部分(咱們姑且叫它 ChatPanel 吧)

其實咱們每次調用res.write()時都會返回一個全新的 ChatPanel 的 HTML 片斷。於此同時,還會附帶一個<style>元素,將以前的 ChatPanel 設爲display: none。因此看起來像是更新了原來的 ChatPanel 的內容,但實際上是 append 了一個新的,同時隱藏以前的 ChatPanel。

2.5. 點擊重複的按鈕

到目前爲止,基本的方案都有了,但還有一個重要的問題:

在 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');
}
複製代碼

2.6. 存儲

爲了可以保存未發送的內容(點擊 send 按鈕以前的輸入內容),以及同步歷史消息,須要有個地方存儲用戶輸入。同時咱們還會爲每一個鏈接設定一個惟一的用戶 ID。在原版的 css-only-chat 中使用了 Redis。我在 css-only-chat-node 中爲了簡便,直接存儲在了運行時的內存變量中了。

3. 最後

也許有朋友會問,這個 DEMO 有什麼實用價值麼?能夠發展成一個可用的聊天工具麼?

好吧,其實我以爲沒有太大用。可是裏面涉及到的一些「知識點」到是瞭解下也無妨。咱們天天面對那麼多無趣的需求,偶爾看看這種「有意思」的項目也算是放鬆一下吧。

最後,若是想看具體的運行效果,或者想了解代碼的細節,能夠看這裏:

  • css-only-chat-node:因爲原版是 Ruby 寫的,因此實現了一個 NodeJS 版的便於你們查看
  • css-only-chat:css-only-chat 的原版倉庫,使用 Ruby 實現

Just have fun! 😜

相關文章
相關標籤/搜索