Open steinhardt21 opened 10 months ago
I am successfully using Vuejs router inside extensions for Content Pages, Popup and Options pages, details here: https://github.com/mubaidr/vite-vue3-chrome-extension-v3
This is very ambiguous question, likely outside the scope of this project, and probably should be posted on Stack Overflow instead.
If you need a router in a tab extension page, then use any of the framework router libraries like React Router or svelte-spa-router. I have successfully created a router injected into a content page which checks the page url like github.com/home or github.com/login, matching '/home' and '/login', before calling a function which will take over the page with inject code.
import handleGithubHome from './routes/home'
import handleGithubLogin from './routes/login'
const params = {info: 'about the current context'}
const map = { '/home': handleGithubHome, '/login': handleGithubLogin}
const routerFunc = <some logic to test for '/home' or '/login'
routerFunc(params)
You can easily check the url when the content script is loaded and call a different function to handle that page.
@adam-s how do you check page url from content script? i thought that might be more feasible from background script
@devhandler Here is an example of how I solved it. Watch for URL changes using the background service worker. What is important is using the background script to listen for changes to the url and inform the content script. Then the content script uses window.location.href
.
// Define a function to send URL changes to the 'robinhood.com' tab.
const sendUrlChangeToRobinhood = (
details: chrome.webNavigation.WebNavigationTransitionCallbackDetails
) => {
const url = new URL(details.url);
if (url.hostname === 'robinhood.com') {
chrome.tabs.sendMessage(details.tabId, {
type: 'URL_CHANGE',
message: details.url,
});
}
};
// Add the above function as a listener to chrome's webNavigation.onHistoryStateUpdated event.
chrome.webNavigation.onHistoryStateUpdated.addListener(
sendUrlChangeToRobinhood
);
This implements a custom router which doesn't matter so much as the jist of what it does.
const connection = new ContentScriptConnection('ROBINHOOD');
const api = new API(getHeaders);
const home = new Home(connection, api);
const options = new Options(connection, api);
const optionsChains = new OptionsChains(connection, api);
const stocks = new Stocks(connection, api);
const router = new RobinhoodRouter();
router.addRoute('/', home);
router.addRoute('options/:id', options);
router.addRoute('stocks/:symbol', stocks);
router.addRoute('options/chains/:symbol', optionsChains);
router.start();
The router has a listener that calls the parents route method.
import { Router } from '../lib/Router';
/**
* Class representing a custom router specifically for the Robinhood application.
* Extends from the general Router class.
*/
export class RobinhoodRouter extends Router {
// The previousUrl property keeps track of the last URL the router has processed.
private previousUrl: string;
/**
* Creates a new instance of the RobinhoodRouter.
*/
constructor() {
super();
this.previousUrl = '';
}
/**
* Starts the router.
* It sets up a listener for changes in the history state and initializes the routing process.
*/
start(): void {
// Add a listener for messages from the background script.
// The background script will send a message when the URL changes.
chrome.runtime.onMessage.addListener(
(event: { type: string; message: string }) => {
// If the received message indicates a URL change...
if (event.type === 'URL_CHANGE') {
// Check if the new URL is different from the previous URL.
// This check is necessary because the onHistoryStateUpdated event can be triggered multiple times for a single navigation event.
if (this.previousUrl !== event.message) {
// Update the previousUrl property and route to the new URL.
this.previousUrl = window.location.href;
this.route(event.message);
}
}
}
);
// Kick off the routing process by routing to the current URL.
// Also update the previousUrl property.
this.route(window.location.href);
this.previousUrl = window.location.href;
}
}
This will call the handleRoute method that is provided as the map
export interface IRoute {
handleRoute: (params?: Record<string, string>) => Promise<void>;
destroy?: () => void;
}
export class Router {
routes: Map<string, IRoute>;
currentRoute?: IRoute;
constructor() {
this.routes = new Map();
}
addRoute(route: string, handler: IRoute): void {
this.routes.set(route, handler);
}
async route(url: string): Promise<void> {
// If there's a current route and it has a cleanup function, execute it
if (this.currentRoute && this.currentRoute.destroy) {
this.currentRoute.destroy();
this.currentRoute = undefined;
}
const urlObject = new URL(url);
for (const [route, handler] of this.routes) {
const params = this.matchRoute(urlObject.pathname, route);
if (params) {
// Execute the handler and get the cleanup function, if any
await handler.handleRoute(params);
// Set the current route to the one we just routed to
this.currentRoute = handler;
return;
}
}
}
matchRoute(path: string, route: string): Record<string, string> | null {
const pathSegments = path.split('/').filter(Boolean);
const routeSegments = route.split('/').filter(Boolean);
if (pathSegments.length !== routeSegments.length) {
return null;
}
const params: Record<string, string> = {};
for (let i = 0; i < routeSegments.length; i++) {
if (routeSegments[i].startsWith(':')) {
const paramName = routeSegments[i].slice(1);
params[paramName] = pathSegments[i];
} else if (routeSegments[i] !== pathSegments[i]) {
return null;
}
}
return params;
}
}
Here is an example of the home page implementing the IRoute
interface defining a route.
import { Subscription } from 'rxjs';
import { ContentScriptConnection } from '../../lib/ContentScriptConnection';
import { IRoute } from '../../lib/Router';
import { API } from '../../lib/api/API';
import { OverlayManager } from './OverlayManager';
export class Home implements IRoute {
connector: ContentScriptConnection; // Connects to the background script
api: API;
subscription: Subscription | null = null;
overlayManager: OverlayManager | null = null;
constructor(connector: ContentScriptConnection, api: API) {
this.connector = connector;
this.api = api;
console.log('Home route initialized');
}
handleRoute = async (_params?: Record<string, string>): Promise<void> => {
this.overlayManager = new OverlayManager();
};
handleMessage = (_message: any) => {};
destroy = () => {
if (this.overlayManager) {
this.overlayManager.destroy();
}
console.log('Home route destroyed');
};
}
It works very well. So maybe I'll refactor and publish it.
this makes sense. thank you. one challenge is onHistoryStateUpdated might not fire for regular navigation like reload, etc.
the background has better event support (content script probably can only use mutationobserver), but the content sript is easier to access url unlike background, having to use many tab.url, etc.
one challenge is onHistoryStateUpdated might not fire for regular navigation like reload,
On reload or other types of navigation where the assists, i.e. js and css files, are loaded, the routing code is called when the injected content script is evaluated. Use window.location.href
to initialize and map the url / href to the correct function to call.
// Kick off the routing process by routing to the current URL.
// Also update the previousUrl property.
this.route(window.location.href);
this.previousUrl = window.location.href;
Initialize in the service worker a listener for webNavigation.onHistoryStateUpdated
and send a custom event to the content script with information about the new url / href. This will handle the single page apps which use window.history.pushstate
updating the url / href without reloading.
// Add the above function as a listener to chrome's webNavigation.onHistoryStateUpdated event.
chrome.webNavigation.onHistoryStateUpdated.addListener(
sendUrlChangeToRobinhood
);
Although the initial route is set manually when then content script is injected and evaluated by the browser engine, add a listener in the content script for the url / href changes detected by the service worker.
// Add a listener for messages from the background script.
// The background script will send a message when the URL changes.
chrome.runtime.onMessage.addListener(
(event: { type: string; message: string }) => {
// If the received message indicates a URL change...
if (event.type === 'URL_CHANGE') {
// Check if the new URL is different from the previous URL.
// This check is necessary because the onHistoryStateUpdated event can be triggered multiple times for a single navigation event.
if (this.previousUrl !== event.message) {
// Update the previousUrl property and route to the new URL.
this.previousUrl = window.location.href;
this.route(event.message);
}
}
}
);
This will set the route when the content script is injected and executed AND will leverage the service worker to listen for changes to the url / href passing the information back to the content script which has been injected and executed already to react mapping new url / href to different functions to call.
(note: This question might be better asked on StackOverflow or in a Reddit sub than here since it is a general question.)
Describe the problem
Which is the system that you advise when using this framework for the routing part? What do you think about this library https://github.com/Scout-NU/route-lite?
Describe the proposed solution
A routing system inside a complex Chrome extension that has to manage various pages (Login, Home, etc.) inside a Chrome extension that show up to the user such as an iframe.
Alternatives considered
https://github.com/Scout-NU/route-lite
Importance
nice to have