京東閱讀(web)體驗優化

發表於2020-07-23

京東有電子書可以購買,可以多端閱讀。比如PC客戶端,移動端,以及本文提到的PC網站端

先換個鏡頭,讀書要記筆記(電子版本), 方便以後查閱。

鏡頭換回來,但是,我們為了方便肯定是想複製,下載啊,分享啊等,但是服務商一般是不允許你這麼做的。

我了,在京東買了幾本書,程式相關的,為了獲取好的體驗,在PC網站端閱讀, 發現精彩之處,想去複製到筆記裡面去。

結果,呵呵噠,結果連選中都不讓。

更關鍵的是,這程式碼部分的顯示是這樣的。 辣眼睛啊。

所以,我打算hack一些,提升閱讀體驗。

  1. 允許選中
  2. 允許快捷複製, Control + C
  3. 允許右鍵複製
  4. 美化程式碼

經過網頁的內容和節點分析,京東電子書PC網站端,是採用普通的div, p ,code等html標籤,而不是pdf的外掛或者canvas等。
那麼我就有信心把你搞得面目全非,錯了,服服帖帖。

1. 允許選中

原理

是通過在div上的style user-select: none 來實現的

<div class="JD_page" style="width: 675px;overflow: hidden;height: 100%;float: left;background-color: rgb(240, 240, 240);margin-top: 5px;font-size: 16px;/* user-select: none; */z-index: 0;" ... >....</div>

方案

那麼就好辦了,音樂起。為了省去麻煩,來個暴力模式。

* {
    user-select: auto !important;
 }

之後,就是建立一個style的標籤,寫入樣式,掛載到head或者body裡面就ok拉。

2. 允許快捷複製

原理:

攔截keydown,讓你的鍵盤事件失靈。

方案:

  1. F12手動刪除註冊keydown的事件
  2. 程式碼刪除註冊keydown的事件

這裡採用2方案,問題來了,如何找到某個元素註冊的事件。
chrome 控制檯提供了一個 getEventListeners的方法,有那味了,戰歌起:

    // 刪除監聽事件
    function cRemoveListener(el, option) {
        if (!el || !option) {
            return;
        }
        el.removeEventListener(option.type, option.listener, option.useCapture || false);
    }

    // 刪除指定的監聽事件
    function cRemoveListeners(el, eventName) {
        var allListeners = getEventListeners(document);
        var listeners = allListeners[eventName];
        if (listeners && listeners.length > 0) {
            for (let i = listeners.length - 1; i >= 0; i--) {
                const lsOption = listeners[i];
                cRemoveListener(el, lsOption);
            }
        }
    }

    // 允許 ctl + c 複製
    // document.body keydown 事件
    cRemoveListeners(document, "keydown");

3. 允許右鍵複製

原理

右鍵選單一般都是通過contextmenu事件,所以同上

方案

同允許快捷複製

    // 允許右鍵
    // document.body contextmenu 事件
    cRemoveListeners(document, "contextmenu");

4. 美化程式碼

原理

京東電子書,是對程式碼部分使用code標籤來展示的。

方案

為了保持斷行,只需要使用pre標籤來包裹一下。
簡單的包裹會產生兩個問題

  1. 包裹一下後,程式碼佔據的頁面內容會變長,而京東電子書這塊,限定了一個頁面的高度為900px,超過部分隱藏。
    所以,我們在使用pre包裹code節點的同事,還需要調整頁面塊這裡的樣式。
  2. 電子是採取的分頁載入,在分頁載入之後,我們需要對新生成的code標籤進行包裹。

包裹code元素的思路

  1. 選擇出所有帶id的code節點(經過觀察,code節點分兩類,一類是有id標籤,一類是沒有,簡單說就是對應markdown裡面的 ``` 和 `)
  2. 找到每個code節點的父節點
  3. 建立pre節點
  4. 插入pre節點到code節點之前
  5. code節點 掛載到 pre下
  6. code新增code-hacked class,標籤已經被hacked,避免重複被hacked

戰歌起,上程式碼

    // 建立節點
    function createElement(tagName) {
        const el = document.createElement(tagName);
        return el;
    }

    // 包裹code節點
    function adoptCodeNode(el) {
        if (!el || el.tagName !== "CODE") return;

        const parent = el.parentElement;
        // 節點前插入
        const preElement = createElement("pre");
        preElement.classList.add("pre-hacked");
        parent.insertBefore(preElement, el);
        // 匯入節點
        preElement.appendChild(el);
        el.classList.add("code-hacked");
    };
    
    function adoptAllCodes() {
        const codesEls = Array.from(document.querySelectorAll("code[id]:not(.code-hacked)"));
        for (let i = codesEls.length - 1; i >= 0; i--) {
            adoptCodeNode(codesEls[i]);
        }
    }

    adoptAllCodes();

分頁載入後的想到的方案

  1. 可以起個定時器,幾秒處理一下
  2. 監聽document.scrollingElement(document.body)的高度變化
  3. 監聽document.scrollingElement(document.body)的scroll事件
  4. 採用MutationObserver監聽子節點是否有變化
  5. 攔截分頁資料的HTTP請求
  6. 攔截執行滾動載入的事件

第一種方式簡單粗暴,其實我很喜歡。
第二種方式不太好實現,分頁載入後,window本身沒有觸發resize事件,window外的節點本身沒有監聽resize的能力(IE除外),當然可以通過 節點resize監聽, 但是高度的變化依舊沒法。
第三種方式,倒是可行,不過scroll事件觸發頻率很高,當然可以節流,也還不錯。
第四種 ,可行性高,PC相容性行也不錯,效能也相對好一點。
第五種, 程式碼複雜度會高一些。

戰歌起:


    // 監聽高度變化
    // https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver
    function hackLoadmore() {
        const targetNode = document.scrollingElement;

        // 觀察器的配置(需要觀察什麼變動)
        const config = { childList: true, subtree: true };

        let preScrollHeight = targetNode.scrollHeight;
        // 當觀察到變動時執行的回撥函式
        const callback = function (mutationsList, observer) {
            // Use traditional 'for loops' for IE 11
            console.log("MutationObserver");
            for (let mutation of mutationsList) {
                if (mutation.type !== 'childList') {
                    return;
                }
                const scollHeight = targetNode.scrollHeight
                if (scollHeight == preScrollHeight) {
                    return;
                }
                preScrollHeight = scollHeight;
                setTimeout(() => {
                    adoptAllCodes();
                }, 2000)
            }

        };

        // 建立一個觀察器例項並傳入回撥函式
        const observer = new MutationObserver(callback);

        // 以上述配置開始觀察目標節點
        observer.observe(targetNode, config);
    }

到此為止,四個hack都解釋完畢,來一份完整的程式碼:

(function jjjjddddhhhhaaaacccckkkk() {
    const jdBookHackKey = "hoho-jd-book-hack";

    // 刪除監聽事件
    function cRemoveListener(el, option) {
        if (!el || !option) {
            return;
        }
        el.removeEventListener(option.type, option.listener, option.useCapture || false);
    }

    // 刪除指定的監聽事件
    function cRemoveListeners(el, eventName) {
        var allListeners = getEventListeners(document);
        var listeners = allListeners[eventName];
        if (listeners && listeners.length > 0) {
            for (let i = listeners.length - 1; i >= 0; i--) {
                const lsOption = listeners[i];
                cRemoveListener(el, lsOption);
            }
        }
    }

    function createHackStyle() {
        // 允許選擇
        var styleEl = document.createElement("style");
        styleEl.textContent = `
                /*  允許選擇 */
                * {
                        user-select: auto !important;
                  }

                  /*  pre 節點樣式 */
                  pre.pre-hacked {
                      margin:0;
                      padding:0;
                      border:none
                  }

                  .page_container.page_container{
                      height: auto !important;
                      overflow: hidden;
                  }

                   /* code 正常佈局後,會導致單個Page邊長  */
                  .JD_page.JD_page{
                    overflow: auto; 
                    height: auto !important;
                  }
                `
        document.body.append(styleEl);

    }

    // 建立節點
    function createElement(tagName) {
        const el = document.createElement(tagName);
        return el;
    }

    // 包裹code節點
    function adoptCodeNode(el) {
        if (!el || el.tagName !== "CODE") return;

        const parent = el.parentElement;
        // 節點前插入
        const preElement = createElement("pre");
        preElement.classList.add("pre-hacked");
        parent.insertBefore(preElement, el);
        // 匯入節點
        preElement.appendChild(el);
        el.classList.add("code-hacked");
    };

    function adoptAllCodes() {
        const codesEls = Array.from(document.querySelectorAll("code[id]:not(.code-hacked) "));
        for (let i = codesEls.length - 1; i >= 0; i--) {
            adoptCodeNode(codesEls[i]);
        }
    }


    // 監聽高度變化
    // https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver
    function hackLoadmore() {
        const targetNode = document.scrollingElement;

        // 觀察器的配置(需要觀察什麼變動)
        const config = { childList: true, subtree: true };

        let preScrollHeight = targetNode.scrollHeight;
        // 當觀察到變動時執行的回撥函式
        const callback = function (mutationsList, observer) {
            // Use traditional 'for loops' for IE 11
            console.log("MutationObserver");
            for (let mutation of mutationsList) {
                if (mutation.type !== 'childList') {
                    return;
                }
                const scollHeight = targetNode.scrollHeight
                if (scollHeight == preScrollHeight) {
                    return;
                }
                preScrollHeight = scollHeight;
                setTimeout(() => {
                    adoptAllCodes();
                }, 2000)
            }

        };

        // 建立一個觀察器例項並傳入回撥函式
        const observer = new MutationObserver(callback);

        // 以上述配置開始觀察目標節點
        observer.observe(targetNode, config);
    }


    function hackhackhack() {

        if (window[jdBookHackKey]) {
            console.log("已經修復,無需再修復");
            return;
        }
        window[jdBookHackKey] = true;

        // 建立style節點
        createHackStyle();

        // 允許 ctl + c 複製
        // document.body keydown 事件
        cRemoveListeners(document, "keydown");


        // 允許右鍵
        // document.body contextmenu 事件
        cRemoveListeners(document, "contextmenu");

        // 調整code節點
        adoptAllCodes();

        // 頁面載入更多內容時
        setTimeout(() => {
            hackLoadmore();
        }, 0)

    }

    hackhackhack();

})()

基本的問題都解決了,上圖。

上圖可以看到

  • 程式碼已經格式化
  • 可以右鍵選擇
    當然ctrl + c這種效果用截圖是表達不出來的,得視訊,但是木有。

上圖,可以看到,因為程式碼被格式化,頁面邊長,但是內容都已經能完整顯示。

最後,感謝大家的閱讀,也希望能幫助到大家。

哦,忘了,怎麼使用,還是截圖。

相關文章