跳到正文
DEV Community· Emir Mrkaljević·· 3 小時前AI 評分52

ShrinkWatch EU 如何在 MV3 擴充功能中處理單頁導覽

Your content script ran once. The shop never reloaded: handling single-page navigation in an MV3 extension

AI 導讀

ShrinkWatch EU 0.3.5 修正 Mercadona 網店切換至商品頁而不重新載入時,擴充功能徽章未顯示的問題,改為讓內容腳本在整個網店持續執行,並按路徑判斷頁面狀態。

正文

Emir Mrkaljević

ShrinkWatch EU is a small open-source extension that shows the price per kg or litre on European supermarket web shops and warns when a product got smaller than the last time you saw it. In 0.3.5 I fixed a bug that every extension author hits sooner or later.

The bug

On Mercadona's web shop you search for "leche", click a product, and the URL changes to /product/3400/.... But no page load happens. The shop is a single-page app, so it calls history.pushState and redraws.

My content script is injected once, at document_idle. It checked location.pathname against the shop's product pattern, saw /search-results, and quit. So the badge only showed up if you reloaded the product page.

Why not just patch history.pushState?

Content scripts live in an isolated world. Wrapping history.pushState there does not see the page's own calls. You would need to inject a script into the page's main world, which is more code, more review questions from add-on stores, and more ways to break a shop.

What I did instead

  1. Stay alive on the whole shop. The script no longer exits on non-product pages. It decides what to do from a small pure function:
SW.pageMode = (adapter, pathname, forced) => ({
  product: !!forced || !!(adapter.productPath && adapter.productPath.test(pathname)),
  listing: !!adapter.listing,
});
  1. Watch the path, not the URL. A cheap 700 ms check plus a popstate listener for back/forward. Query and hash changes are ignored, because shops love adding tracking parameters.
SW.watchPath = (getPath, onChange, { interval = 700, window } = {}) => {
  let last = getPath();
  const check = () => { const now = getPath(); if (now !== last) { const prev = last; last = now; onChange(now, prev); } };
  const id = setInterval(check, interval);
  window?.addEventListener('popstate', check);
  return { check, stop() { clearInterval(id); window?.removeEventListener('popstate', check); } };
};
  1. Cancel stale work. The product reader retries for a while (prices often render late). Each path change bumps a counter, and an old retry loop that sees a newer counter stops, so it can never paint the previous product's numbers on the new page.

  2. Remove the old badge on every path change, then read again if the new path is a product.

Testing it without a browser

Because the watcher takes getPath, setInterval and window as inputs, node --test can drive it with fake timers and a fake popstate. No headless browser needed for the logic.

Privacy note

None of this makes a network request. The extension only reads the page you are already looking at and keeps history in local storage.

This article was written with AI assistance. The code, tests and examples come from the repo.

來源:DEV Community · dev.to