惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Apple Machine Learning Research
Apple Machine Learning Research
aimingoo的专栏
aimingoo的专栏
H
Help Net Security
腾讯CDC
T
Tailwind CSS Blog
Hugging Face - Blog
Hugging Face - Blog
人人都是产品经理
人人都是产品经理
酷 壳 – CoolShell
酷 壳 – CoolShell
MongoDB | Blog
MongoDB | Blog
宝玉的分享
宝玉的分享
有赞技术团队
有赞技术团队
美团技术团队
雷峰网
雷峰网
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
博客园 - 司徒正美
博客园_首页
Recent Announcements
Recent Announcements
云风的 BLOG
云风的 BLOG
B
Blog RSS Feed
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
D
Docker
博客园 - Franky
Jina AI
Jina AI

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Structuring Modern Browser Extensions for Maintainability...
Ran Tayeb · 2026-05-17 · via DEV Community

 Browser extensions often begin as simple utilities—a few listeners in a background script, a small content script, and maybe a popup. But as the extension grows to include multiple runtime contexts, more messaging, state, and browser-specific behavior, the codebase can quickly become difficult to reason about.

That problem gets sharper in modern extensions because the application is split across isolated environments:

  • Background
  • Content
  • Managed UI such as popup or DevTools

These are not just folders. They are separate runtime contexts with different capabilities and lifecycles.

That’s where architecture starts to matter.

HexaJS is a structured, decorator-driven framework for browser extensions. It brings together Dependency Injection, tokens, controllers, handlers, and an AOT build pipeline so extension code stays modular, predictable, and scalable.

Why structure matters in browser extensions

In a typical extension, complexity grows in a few familiar ways:

  • message names become scattered strings
  • browser APIs leak into too many places
  • content and background responsibilities get mixed together
  • configuration is hidden in globals or utility modules
  • testing gets harder as dependencies become implicit

HexaJS addresses this by treating runtime boundaries as first-class architecture.

Its docs describe a model where Background, Content, and UI each run with their own generated bootstrap and isolated DI container. That means a class valid in one context is not automatically valid in another.

Dependency Injection with context awareness

HexaJS uses a lightweight decorator-based DI model.

A service can be declared with an explicit runtime context:

import { Injectable, HexaContext } from '@hexajs-dev/common';

@Injectable({ context: HexaContext.Background })
export class TabQueryService {
  getActiveIdFromTabs(tabs: Array<{ id?: number }>): number {
    return tabs[0]?.id ?? -1;
  }
}

Enter fullscreen mode Exit fullscreen mode

This is important because browser extensions are inherently multi-context. A background service should not accidentally be injected into content or UI code if it depends on background-only capabilities.

HexaJS validates those boundaries during its AOT build step.

So DI here is not just about cleaner constructors—it is also about enforcing architectural correctness before runtime.

Tokens make configuration explicit

Not every dependency is a class. Some are values: platform, build mode, feature flags, or API URLs.

HexaJS supports token-based injection for those cases.

Built-in system tokens include values like HEXA_PLATFORM and HEXA_BUILD_MODE, and custom tokens can be created with createToken(...).

import { createToken, Inject, Injectable, HexaContext } from '@hexajs-dev/common';

export const API_BASE_URL = createToken(
  'API_BASE_URL',
  'https://api.example.com',
  HexaContext.Background
);

@Injectable({ context: HexaContext.Background })
export class ApiConfigService {
  constructor(@Inject(API_BASE_URL) private apiBaseUrl: string) {}

  getBaseUrl(): string {
    return this.apiBaseUrl;
  }
}

Enter fullscreen mode Exit fullscreen mode

This keeps configuration explicit and testable.

Instead of pulling values from scattered globals, the dependency is visible in the constructor and can be overridden through configuration layers in the HexaJS build setup.

Background communication with Controllers

In HexaJS, background-side message endpoints are modeled with Controllers and Actions.

import { Controller, Action } from '@hexajs-dev/core';
import { TabsPort } from '@hexajs-dev/ports';

@Controller({ namespace: 'tabs' })
export class TabsController {
  constructor(private tabsPort: TabsPort) {}

  @Action('active')
  async activeTab(): Promise<{ tabId: number }> {
    const tabs = await this.tabsPort.queryTabs({ active: true, currentWindow: true });
    return { tabId: tabs[0]?.id ?? -1 };
  }
}

Enter fullscreen mode Exit fullscreen mode

This is one of the biggest corrections from the original draft:

  • it is @Controller({ namespace: '...' }), not @Controller('...')
  • it is @Action('...'), not @Message('...')

The resulting route is effectively namespaced as:

tabs:active

Enter fullscreen mode Exit fullscreen mode

That makes background communication more structured than manually wiring chrome.runtime.onMessage handlers or large switch statements.

HexaJS also recommends using ports instead of direct chrome.* or browser.* API calls, which keeps controller logic focused on orchestration rather than browser-specific plumbing.

Content behavior with Handlers

On the content side, HexaJS uses Handlers and Handle decorators.

import { Handler, Handle } from '@hexajs-dev/core';
import { LoggerService } from '../services/logger.service';
import { MyContentEntry } from './content';

@Handler({ namespace: 'tabs', Contents: [MyContentEntry] })
export class TabsHandler {
  constructor(private logger: LoggerService) {}

  @Handle('active-id')
  onActiveId(payload: { tabId: number }): { ok: boolean } {
    this.logger.log('Active tab id from background:', payload.tabId);
    return { ok: true };
  }
}

Enter fullscreen mode Exit fullscreen mode

This is another important fix from the original version:

  • it is @Handler({ namespace, Contents }), not @Handler('page')
  • it is @Handle('...'), not @Action('...') inside content handlers

The Contents option is especially useful because content scripts are often more fragmented than background logic. A single extension may have multiple content entry classes for different sites or page families, and HexaJS lets handlers bind intentionally to those entries.

That makes large multi-site extensions much easier to organize.

Clean transport across contexts

A common problem in browser extension code is that transport logic ends up mixed into business logic. Raw chrome.runtime.sendMessage(...) calls get scattered everywhere, and eventually every feature carries its own messaging details.

HexaJS pushes toward structured routing instead.

For UI surfaces, the docs show using HexaUIClient via DI:

import { inject } from '@hexajs-dev/common';
import { HexaUIClient } from '@hexajs-dev/ui';

const uiClient = inject(HexaUIClient);

const result = await uiClient.sendMessage('tabInfo:current', {});
console.log('Active tab metadata:', result);

Enter fullscreen mode Exit fullscreen mode

This is another correction from the original example: I could not verify HexaContentClientService in the repo/docs you pointed me to, so I would avoid naming it in the post unless you specifically want to introduce it separately and confirm it exists in your public API.

What is clearly documented is the idea that UI and content layers communicate through structured routes, while browser-specific behavior is delegated through ports and handled in the appropriate context.

Why this scales better

This kind of structure helps browser extensions scale in a few practical ways:

1. Better separation of concerns

Controllers stay background-focused. Handlers stay content-focused. Services hold reusable logic. Tokens represent values and configuration.

2. Easier testing

DI makes dependencies explicit, so services and ports are easier to mock.

3. Safer refactoring

When route definitions and context boundaries are structured, it becomes much easier to move code without accidentally crossing runtime boundaries.

4. Reduced browser-specific coupling

Ports isolate platform details so the rest of the application can focus on behavior.

5. Long-term maintainability

The framework is designed less for the fastest possible hello-world and more for the codebase you can still understand months later.

Final thought

Browser extensions are no longer tiny scripts.

They are multi-context applications with isolated runtimes, lifecycle boundaries, and transport concerns. If the architecture does not reflect that reality, complexity spreads fast.

HexaJS approaches that problem with a structured model:

  • Dependency Injection for explicit dependencies
  • Tokens for values and configuration
  • Controllers for background routes
  • Handlers for content-side behavior
  • AOT analysis for context-aware validation and generated wiring

The result is a browser extension codebase that is easier to reason about, easier to test, and easier to scale.

If you’re building modern browser extensions and want a more structured approach, check out HexaJS.


Quick reference snippets

Background service

import { Injectable, HexaContext } from '@hexajs-dev/common';

@Injectable({ context: HexaContext.Background })
export class TabQueryService {
  getActiveIdFromTabs(tabs: Array<{ id?: number }>): number {
    return tabs[0]?.id ?? -1;
  }
}

Enter fullscreen mode Exit fullscreen mode

Token injection

import { Inject, Injectable, HEXA_PLATFORM } from '@hexajs-dev/common';

@Injectable()
export class PlatformLabelService {
  constructor(@Inject(HEXA_PLATFORM) private platform: string) {}

  getLabel(): string {
    return `running on ${this.platform}`;
  }
}

Enter fullscreen mode Exit fullscreen mode

Background controller

import { Controller, Action } from '@hexajs-dev/core';
import { TabsPort } from '@hexajs-dev/ports';

@Controller({ namespace: 'tabs' })
export class TabsController {
  constructor(private tabsPort: TabsPort) {}

  @Action('active')
  async activeTab(): Promise<{ tabId: number }> {
    const tabs = await this.tabsPort.queryTabs({ active: true, currentWindow: true });
    return { tabId: tabs[0]?.id ?? -1 };
  }
}

Enter fullscreen mode Exit fullscreen mode

Content handler

import { Handler, Handle } from '@hexajs-dev/core';
import { MyContentEntry } from './content';

@Handler({ namespace: 'tabs', Contents: [MyContentEntry] })
export class TabsHandler {
  @Handle('active-id')
  onActiveId(payload: { tabId: number }): { ok: boolean } {
    return { ok: true };
  }
}

Enter fullscreen mode Exit fullscreen mode