1. 程式人生 > >京東閱讀(web)體驗優化

京東閱讀(web)體驗優化

京東有電子書可以購買,可以多端閱讀。比如PC客戶端,移動端,以及本文提到的**PC網站端**。 先換個鏡頭,讀書要記筆記(電子版本), 方便以後查閱。 鏡頭換回來,但是,我們為了方便肯定是想複製,下載啊,分享啊等,但是服務商一般是不允許你這麼做的。 我了,在京東買了幾本書,程式相關的,為了獲取好的體驗,在**PC網站端**閱讀, 發現精彩之處,想去複製到筆記裡面去。 結果,呵呵噠,結果連選中都不讓。 更關鍵的是,這程式碼部分的顯示是這樣的。 辣眼睛啊。 ![](https://img2020.cnblogs.com/blog/1097840/202007/1097840-20200723150703315-1770373657.png) 所以,我打算hack一些,提升閱讀體驗。 1. 允許選中 2. 允許快捷複製, Control + C 3. 允許右鍵複製 4. 美化程式碼 經過網頁的內容和節點分析,京東電子書PC網站端,是採用普通的div, p ,code等html標籤,而不是pdf的外掛或者canvas等。 那麼我就有信心把你搞得面目全非,錯了,服服帖帖。 ### 1. 允許選中 #### 原理 是通過在div上的style `user-select: none` 來實現的 ``` .... ``` #### 方案 那麼就好辦了,音樂起。為了省去麻煩,來個暴力模式。 ```css * { user-select: auto !important; } ``` 之後,就是建立一個style的標籤,寫入樣式,掛載到head或者body裡面就ok拉。 ### 2. 允許快捷複製 #### 原理: 攔截keydown,讓你的鍵盤事件失靈。 #### 方案: 1. F12手動刪除註冊keydown的事件 2. 程式碼刪除註冊keydown的事件 這裡採用2方案,問題來了,如何找到某個元素註冊的事件。 chrome 控制檯提供了一個 getEventListeners的方法,有那味了,戰歌起: ```js // 刪除監聽事件 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 戰歌起,上程式碼 ```js // 建立節點 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監聽](https://xiangwenhu.github.io/TakeItEasy/resize/), 但是高度的變化依舊沒法。 第三種方式,倒是可行,不過scroll事件觸發頻率很高,當然可以節流,也還不錯。 第四種 ,可行性高,PC相容性行也不錯,效能也相對好一點。 第五種, 程式碼複雜度會高一些。 戰歌起: ```js // 監聽高度變化 // 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都解釋完畢,來一份完整的程式碼: ```js (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(); })() ``` 基本的問題都解決了,上圖。 ![](https://img2020.cnblogs.com/blog/1097840/202007/1097840-20200723150727609-161705305.jpg) 上圖可以看到 * 程式碼已經格式化 * 可以右鍵選擇 當然ctrl + c這種效果用截圖是表達不出來的,得視訊,但是木有。 ![](https://img2020.cnblogs.com/blog/1097840/202007/1097840-20200723150734474-1737488262.png) 上圖,可以看到,因為程式碼被格式化,頁面邊長,但是內容都已經能完整顯示。 最後,感謝大家的閱讀,也希望能幫助到大家。 哦,忘了,怎麼使用,還是截圖。 ![](https://img2020.cnblogs.com/blog/1097840/202007/1097840-20200723150743778-7561679