京東閱讀(web)體驗優化(轉載)
京東有電子書可以購買,可以多端閱讀。比如PC客戶端,移動端,以及本文提到的PC網站端。
先換個鏡頭,讀書要記筆記(電子版本), 方便以後查閱。
鏡頭換回來,但是,我們為了方便肯定是想複製,下載啊,分享啊等,但是服務商一般是不允許你這麼做的。
我了,在京東買了幾本書,程式相關的,為了獲取好的體驗,在PC網站端閱讀, 發現精彩之處,想去複製到筆記裡面去。
結果,呵呵噠,結果連選中都不讓。
更關鍵的是,這程式碼部分的顯示是這樣的。 辣眼睛啊。
所以,我打算hack一些,提升閱讀體驗。
- 允許選中
- 允許快捷複製, Control + C
- 允許右鍵複製
- 美化程式碼
經過網頁的內容和節點分析,京東電子書PC網站端,是採用普通的div, p ,code等html標籤,而不是pdf的外掛或者canvas等。
1. 允許選中
原理
是通過在div上的styleuser-select: none
來實現的
<div class="JD_page" style="width: 675px;overflow: hidden;height: 100%;float: left;margin-top: 5px;font-size: 16px;/* user-select: none; */z-index: 0;" ... >....</div>
方案
那麼就好辦了,音樂起。為了省去麻煩,來個暴力模式。
* {
user-select: auto !important;
}
之後,就是建立一個style的標籤,寫入樣式,掛載到head或者body裡面就ok拉。
2. 允許快捷複製
原理:
攔截keydown,讓你的鍵盤事件失靈。
方案:
- F12手動刪除註冊keydown的事件
- 程式碼刪除註冊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標籤來包裹一下。
簡單的包裹會產生兩個問題
- 包裹一下後,程式碼佔據的頁面內容會變長,而京東電子書這塊,限定了一個頁面的高度為900px,超過部分隱藏。
所以,我們在使用pre包裹code節點的同事,還需要調整頁面塊這裡的樣式。 - 電子是採取的分頁載入,在分頁載入之後,我們需要對新生成的code標籤進行包裹。
包裹code元素的思路
- 選擇出所有帶id的code節點(經過觀察,code節點分兩類,一類是有id標籤,一類是沒有,簡單說就是對應markdown裡面的 ``` 和 `)
- 找到每個code節點的父節點
- 建立pre節點
- 插入pre節點到code節點之前
- code節點 掛載到 pre下
- 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();
分頁載入後的想到的方案
- 可以起個定時器,幾秒處理一下
- 監聽document.scrollingElement(document.body)的高度變化
- 監聽document.scrollingElement(document.body)的scroll事件
- 採用MutationObserver監聽子節點是否有變化
- 攔截分頁資料的HTTP請求
- 攔截執行滾動載入的事件
第一種方式簡單粗暴,其實我很喜歡。
第二種方式不太好實現,分頁載入後,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這種效果用截圖是表達不出來的,得視訊,但是木有。
上圖,可以看到,因為程式碼被格式化,頁面邊長,但是內容都已經能完整顯示。
最後,感謝大家的閱讀,也希望能幫助到大家。
哦,忘了,怎麼使用,還是截圖。