Giter Site home page Giter Site logo

browser-extension-kit's Introduction

English | 中文

browser-extension-kit

Background

The difficulty in developing Chrome extensions is that there are multiple execution environments:

  • background
  • popup
  • content-script
  • page-script
  • devtool

These environments are isolated from each other, and communication can only be achieved through message. The Chrome extension itself provides a postMessage-based messaging mechanism, but once you've actually used it for a while, you'll find that its API is not that useful. For example:

  • Between different execution environments, it is necessary to distinguish between internal messages and external messages (such as page-script and background communication), and their APIs are different
  • Order of connection establishment: The party that actively establishes the connection should be the party with the shorter life cycle, but the life cycle may be completely opposite in different scenarios, such as between devtools and page-script, which depends entirely on the opening and closing timing of devtools
  • It is not possible to establish a direct connection between any two execution environments, such as page-script -> content-script -> background -> devtools
  • There are various message delivery methods, which need to be used according to the actual situation, such as page-script -> content-script, that is, you can use Chrome's API (requires background transfer), or you can directly use window.postMessage

For these reasons, browser-extension-kit was born - a tool to help you develop Chrome extensions.

Getting-started

Install

npm i browser-extension-kit -S
// or
yarn add browser-extension-kit

Instruction

Before starting development, you need to understand the basic design ideas and composition of Chrome extensions. This tool focuses on solving the biggest pain point in the extension development process - the message passing mechanism. For the rich API provided by Chrome itself, you still need to call it yourself.

In the official design idea of ​​the Chrome extension, a extension contains 5 mutually isolated execution environments:

  • background
  • content-script
  • page-script
  • devtools
  • popup

In browser-extension-kit, according to these 5 execution environments, 3 base classes and 2 React hooks are abstracted:

  • Background: the base class, the logic executed in the background needs to inherit this base class
  • ContentScript: base class, all logic executed in content-script needs to inherit this base class
  • PageScript: base class, all logic executed in page-script needs to inherit this base class
  • useMessage: hooks, used in the execution environment related to the two UI interfaces of devtools and popup
  • usePostMessage: hooks, used in the execution environment related to the two UI interfaces of devtools and popup

The execution environment of the UI class is essentially a React component, and this tool does not impose too many restrictions. For the remaining three execution environments, the static method bootstrap on the corresponding base class needs to be called for initialization.

Demo

Suppose we need to make a extension, this extension needs to poll the server to get some data, and display the data in the popup, and the parameters of the polling interface need to be input by the user in the popup. First, we need to implement the logic in the background, any extension must have a background, and we handle transactions in the background. Typically, you should define all core business logic and state in the background.

// src/background/myBackground.ts
import { interval, from, Subject } from 'rxjs';
import axios from 'axios';
import { Background } from 'browser-extension-kit/background';
import { observable, subject } from 'browser-extension-kit';
import { withLatestFrom, shareReplay, switchMap } from 'rxjs/operators';

export default class MyBackground extends Background {
  // Use @subject('MyBackground::id') to specify to listen for messages with id MyBackground::id
  // and automatically convert the data of this message to the next value of idSubject
  @subject('MyBackground::id') private idSubject = new Subject<string>();

  // Use @observable.popup() to automatically subscribe to data$,
  // and pass the value sent each time to popup through a message,
  // the id of the message defaults to MyBackground::data$
  @observable.popup() private data$ = interval(3000).pipe(
    withLatestFrom(this.idSubject),
    switchMap(([_, id]) => from(axios.get('/api/data', { params: { id }}))),
  ).pipe(
    shareReplay(1)
  );
}
// src/background/index.ts
import { Background } from 'browser-extension-kit/background';
import MyBackground from './MyBackground.ts';

// Use bootstrap to initialize
Background.bootstrap(MyBackground);
// src/popup/App.tsx
import React, { ChangeEvent, useCallback } from 'react';
import { useMessage, usePostMessage } from 'browser-extension-kit/popup';

const App = () => {
  const port = usePostMessage();
  // Accept message with id MyBackground::data$ and specify default value
  const data = useMessage<string>('MyBackground::data$', '');

  const handleChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
    // Send a message with id MyBackground::id to background
    port.background('MyBackground::id', e.target.value);
  }, [port]);

  return (
    <div>
      current data: {data}
      input id param: <input onChange={handleChange} />
    </div>
  );
};

export default App;

Note that in the example here, the background uses rxjs to manage data, you can also not use rxjs, the framework also provides a lower-level API for you to call, see the API documentation for details. ​

API

Background

Background.bootstrap(scripts: Array<new () => Background>): void

Initialize background related functions

frameList$: Subject<FrameList>

An rxjs Subject, the value of which is all frames currently connected

port

  • port.background: <T>(id: string, data?: T) => void
  • port.contentScript: <T>(id: string, data?: T, filter?: MessageFilters) => void
  • port.devtools: <T>(id: string, data?: T, filter?: MessageFilters) => void
  • port.pageScript: <T>(id: string, data?: T, filter?: MessageFilters) => void
  • port.popup: <T>(id: string, data?: T, filter?: MessageFilters) => void

Send a message to the corresponding context, where,

interface MessageFilters {
  groupId?: string | number; // 用户自定义的 ID
  tabId?: number;
  windowId?: number;
  origin?: string;
  url?: string;
  frameId?: number;
  devtoolsId?: string;
  className?: string;
}

on(id: string, cb: (data: any, sender: MessageFilters | undefined, timestamp: number) => void):void

Listen for messages. Among them, sender in cb represents the information of the sender of the message.

ContentScript

ContentScript.bootstrap(scripts: Array<{ class: new () => ContentScript; groupId?: string }>): void

Initialize contentScript related functions

frameList$: Subject<FrameList>

An rxjs Subject, the value of which is all frames currently connected

port

  • port.background: <T>(id: string, data?: T) => void
  • port.contentScript: <T>(id: string, data?: T, filter?: MessageFilters) => void
  • port.devtools: <T>(id: string, data?: T, filter?: MessageFilters) => void
  • port.pageScript: <T>(id: string, data?: T, filter?: MessageFilters) => void
  • port.popup: <T>(id: string, data?: T, filter?: MessageFilters) => void

Send a message to the corresponding context, where,

interface MessageFilters {
  groupId?: string | number; // 用户自定义的 ID
  tabId?: number;
  windowId?: number;
  origin?: string;
  url?: string;
  frameId?: number;
  devtoolsId?: string;
  className?: string;
}

on(id: string, cb: (data: any, sender: MessageFilters | undefined, timestamp: number) => void, filters?: MessageFilters): void

Listen for messages, where sender in cb represents the information of the sender of the message, and filters can specify only certain messages by filtering

injectPageScript(path: string): void

inject script into page

PageScript

PageScript.bootstrap(extensionId: string, scripts: Array<{ class: new () => PageScript; groupId?: string }): void

Initialize pageScript related functions

frameList$: Subject<FrameList>

An rxjs Subject, the value of which is all frames currently connected

port

  • port.background: <T>(id: string, data?: T) => void
  • port.contentScript: <T>(id: string, data?: T, filter?: MessageFilters) => void
  • port.devtools: <T>(id: string, data?: T, filter?: MessageFilters) => void
  • port.pageScript: <T>(id: string, data?: T, filter?: MessageFilters) => void
  • port.popup: <T>(id: string, data?: T, filter?: MessageFilters) => void

Send a message to the corresponding context, where,

interface MessageFilters {
  groupId?: string | number; // 用户自定义的 ID
  tabId?: number;
  windowId?: number;
  origin?: string;
  url?: string;
  frameId?: number;
  devtoolsId?: string;
  className?: string;
}

on(id: string, cb: (data: any, sender: MessageFilters | undefined, timestamp: number) => void, filters?: MessageFilters): void

Listen for messages, where sender in cb represents the information of the sender of the message, and filters can specify only certain messages by filtering

Popup

useMessage<T = any>(id: string, initialValue?: T, filters?: MessageFilters): [T, MessageFilters | undefined]

listen for messages

useFrame(context: RuntimeContext.contentScript | RuntimeContext.pageScript | RuntimeContext.devtools): [MessageFilters[]]

All frames currently connected

usePostMessage()

The returned data is consistent with Background.port

Devtools

useMessage<T = any>(id: string, initialValue?: T, filters?: MessageFilters): [T, MessageFilters | undefined]

listen for messages

useFrame(context: RuntimeContext.contentScript | RuntimeContext.pageScript | RuntimeContext.devtools): [MessageFilters[]]

All frames currently connected

usePostMessage()

The returned data is consistent with Background.port

Decorators

Available in subclasses of Background ContentScript PageScript

observable(to: Array<'background' | 'contentScript' | 'devtools' | 'pageScript' | 'popup'>, id?: string)

Automatically wrap the corresponding property. The property must be an rxjs Observable. When the value changes, it will automatically send a message to the context specified by to. The message id can be customized

It corresponds to the following simple method:

  • observable.background
  • observable.contentScript
  • observable.devtools
  • observable.pageScript
  • observable.popup

subject(id: string)

Automatically listen for messages, the decorated property must be an rxjs Subject

License

MIT

browser-extension-kit's People

Contributors

alibaba-oss avatar mhcgrq avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

browser-extension-kit's Issues

感觉是挺好的项目,必须会 rxjs 才能用吗?

没写过 angular,感觉 rxjs 挺难的,现在有个场景,两个数据源 subject 都是 array,现在我直接想把两个 array 放一起变成只有一层的 array,数据源变化就更新再放一起

export default class MyBackground extends Background {
  @subject('MyBackground::localStorage')
  private storageSubject = new Subject<[string, string][]>()

  @subject('MyBackground::cookies')
  private cookieSubject = new Subject<[string, string][]>()

  @observable(['popup', 'contentScript'])
  private combinator = this.storageSubject.pipe(
    combineLatestWith(this.cookieSubject),
  )
}

看了好几天 rxjs,拿到的数据结构是 [[string, string][], undefined],总是嵌套着数组
请问如果 combinator 只要 [string, string][] 的结构应该是咋写

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.