简体   繁体   中英

Chrome API in React functional component not working

I am trying to make a chrome extension and would love to use React, but after a full week of running into dead ends this seems to not be the ideal route. Thus, I decided to ask for a few suggestions here from you fine SO users as a last ditch effort before simply using Vanilla JS.

Here is my public/manifest.json file (I manually change "js":[] field to match the build folder's name for now):

{
  "manifest_version": 2,
  "name": "Name",
  "version": "0.1.0",

  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["/static/js/main.356978b8.chunk.js"]
    }
  ],

  "background": {
    "scripts": ["background.js"]
  },

  "permissions": ["tabs", "http://*/"],

  "browser_action": {
    "default_title": "Title"
  }
}

My thinking is that the content script would be all the React function component files and the overall index.js using the above.

Here is package.json :

{
  "name": "client",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "react": "^17.0.1",
    "react-dom": "^17.0.1",
    "react-scripts": "4.0.0"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ],
    "env": {
      "webextensions": true
    }
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

Here is my folder structure:

文件夹结构

src folder :

// src/App.js
function App() {
  return <div>Hello</div>;
}

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  console.log(request.tabs);
});

export default App;

// src/index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById("root")
);

public folder :

// public/background.js
chrome.browserAction.onClicked.addListener((tab) => {
  chrome.tabs.create(
    {
      windowId: null,
      url: "http://localhost:3000",
      active: true,
      openerTabId: tab.id,
    },
    (newTab) => {

      // wait for tab to load, then send message with tabs
      chrome.tabs.onUpdated.addListener((tabId, changeInfo) => {
        if (changeInfo.status == "complete" && tabId == newTab.id) {
          chrome.tabs.getAllInWindow(null, (tabs) => {
            chrome.tabs.sendMessage(newTab.id, { tabs });
          });
        }
      });
    }
  );
});

// public/index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />

    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />

    <title>Title</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

My background.js simply opens a new tab to http://localhost:3000 when the extension button is clicked with chrome.browserAction.onClicked.addListener((tab)=>{...}) . This works fine .

Adding the build folder to Load Unpacked in Chrome Extensions produces the extension, but when I click on the button and the new tab is created, I get

TypeError: Cannot read property 'addListener' of undefined

I know that npm run eject can be used to automate/set the filename in /static/js/... , but for now I am fine with simply changing it on every build change.

Any clues or ideas would be much appreciated!

The problem is that your extension uses two copies of the same React app JS file ( "static/js/main.*.chunk.js" ) that run in different contexts. One copy runs as a content script injected into active page and this one should run without errors. But there is also another copy that runs within CRA's dev server ( localhost:3000 ) you open in a new tab. And this one runs as ordinary JS file without any connection to your extension, so it cannot call Chrome API. That's why you get that error.

A possible solution would be to split that JS file into two separate parts: one (ordinary JS related to index.html ) - to do React rendering only, another (content script) - to listen messages from extension. Communication between such two parts can be done using window.postMessage . In your case something like below:

public/manifest.json:

{
...
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["contentScript.js"]
    }
  ],
...
}

public/contentScript.js:

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  window.postMessage({ type: "FROM_EXT", tabs: request.tabs }, "http://localhost:3000");
});

src/index.js:

...
window.addEventListener("message", event => {
  if (event.source != window)
    return;
  const {type, tabs} = event.data;
  if (type !== "FROM_EXT")
    return;
  console.log(tabs);
});

Furthermore I'd suggest you to use custom CRA template - complex-browserext (plug). It facilitates usage of Create React App with browser extensions. Also see this article for particular usage example (another plug).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM