對於單頁應用中如何監聽 URL 變化的思考
周末開發了一個在 GitHub 中給 repo 增加自定義備註的 chrome 擴展。
開發這個擴展的原因是我在 GitHub 中所 star 的項目實在太多了(截止目前 671 個),有的項目過個幾天回來看就忘了為什麽 star 了,有的輪子想找的時候發現忘記叫什麽了,這麽多一個個找實在浪費時間。於是我一直在想有這麽個工具,可以自定義對 GitHub 中的項目進行備註,然後可以根據備註進行搜索,於是便有了這個擴展。
如需安裝體驗請點擊 GitHub Remarks 進行安裝,源碼移步 github-remarks(沒啥參考價值)。
當然本文並不是講這個擴展的制作過程,而是在這過程中碰到的一個問題。
以我的 GitHub 賬戶舉例,在 https://github.com/hanzichi?tab=stars 頁面,我需要插入一些 dom,使得頁面如下:
乍一想,似乎很簡單,在這個頁面插入 content.js 就行,在 manifest.json 中增加配置:
{
"matches": ["*://github.com/.*tabs=stars"],
"js": ["content-scripts/repoDetail.js"],
"css": [],
"run_at" : "document_end"
}
但是問題來了,直接打開頁面 https://github.com/hanzichi?tab=stars 並沒有問題,但是如果從 https://github.com/hanzichi 點擊跳轉到 https://github.com/hanzichi?tab=stars,因為 GitHub 用了 pjax 技術,content.js 其實應該加載在頁面 https://github.com/hanzichi 的,但是那個頁面並沒有參照的 dom 結構(好把我們需要的 dom 插入)。所以問題似乎就轉為了,在 pjax 頁面,如何能夠監聽到頁面 url 的變化
首先一個很簡單的方案是,弄個定時器循環監聽,但是太耗費性能了,pass
因為頁面是 pjax 跳轉,所以 hashchange 這樣的事件自然也用不上;而 pushstate 事件是監聽瀏覽器前進後退的,所以也並不符合場景
有個想法很好,重寫 pushState 事件(代碼來自 How to detect when HTML5‘s history.pushState() is called?):
(function(history){
var pushState = history.pushState;
history.pushState = function(state) {
if (typeof history.onpushstate == "function") {
history.onpushstate({state: state});
}
// ... whatever else you want to do
// maybe call onhashchange e.handler
return pushState.apply(history, arguments);
}
})(window.history);
但是因為 chrome 擴展和源代碼之間並不共享作用域,所以重寫的 history.pushState 方法在原來頁面其實並沒有改變,所以也沒什麽卵用
然後從 chrome API 入手,發現有個 chrome API 可以檢測當前 url 改變的 tab(參考 Chrome extension WebNavigation listener for hash change):
chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
if (changeInfo.url) {
console.log('Tab %d got new URL: %s', tabId, changeInfo.url)
}
})
這段代碼是在 background.js 中,然後一旦偵測到 URL 變化,background.js 和 content.js 通信告知即可。
但是,但是,也有問題,url 一改變確實能監聽到,但是 chrome 擴展需要在相應 dom ready 後才能作用(才能在之前 dom 的基礎上插入新的 dom),你無法檢測到什麽時候 dom 已經準備就緒了,所以在這裏加個定時器延遲作用是可行的,時間需要自己估算下
這個方法不是很優雅,最後想到,pjax 的過程中,dom 肯定有變化,所以監聽 dom 變化是否可行?答案是可以的:
MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
var observer = new MutationObserver(function(mutations, observer) {
let url = location.href
let p = /.*\/\/github.com\/.*\?tab=stars.*/
if (p.test(url)) {
initStarsPage()
}
})
observer.observe(document.getElementById('js-pjax-container'), {
childList: true
})
$(document).ready(() => {
let url = location.href
let p = /.*\/\/github.com\/.*\?tab=stars.*/
if (p.test(url)) {
initStarsPage()
}
})
寫到最後,其實並不是監聽了 URL,而是監聽了 dom 的變化。
最後的最後,意外在 so 上找到 這個答案 似乎就是對應我的問題,大概看了下,除了我舉例的幾個方案外,其實我遺漏了最簡單的一個方案,既然頁面使用了 pjax,那麽直接監聽 pjax:complete 事件即可:
$(document).on('pjax:complete', function() {
let url = location.href
let p = /.*\/\/github.com\/.*\?tab=stars.*/
if (p.test(url)) {
initStarsPage()
}
})
突然讓人感覺,有時候 "真相" 往往總是那麽簡單。
對於單頁應用中如何監聽 URL 變化的思考