chrome插件注入js问题与解决

219 阅读2分钟

最近有个开发谷歌插件的需求,由于在开发的时候遇到一些问题,在此记录一下

问题

  1. 使用react vite 打包报错
  2. 三方包最新版本没有最新的umd包
  3. 注入script顺序会导致页面报错问题

manifest.json

{
  "name": "hang-content",
  "version": "1.0.0",
  "manifest_version": 3,
  "permissions": [
    "activeTab",
    "tabs"
  ],
  "web_accessible_resources": [
    {
      "resources": [
        "react.js",
        "react-dom.js",
        "react-router.js",
        "react-router-dom.js",
        "inject.js"
      ],
      "matches": [
        "*://*/*"
      ]
    }
  ],
  "background": {
    "service_worker": "background.js"
  },
  "action": {
    "default_popup": "index.html",
    "default_icon": {}
  },
  "content_scripts": [
    {
      "matches": [
        "<all_urls>"
      ],
      "js": [
        "content.js"
      ],
      "run_at": "document_end"
    }
  ]
}

content.js

// content.js
function injectCustomElement() {
  // 需要注入的script
  // 按顺序注入,否则会报错
  const scripts = ["react.js", "react-dom.js", "react-router.js", "react-router-dom.js", "inject.js"]
  scripts.map(url => {
    console.log(`注入 ==> ${url}`);
    const script = document.createElement('script');
    script.src = chrome.runtime.getURL(url);
    script.onload = () => {
      // 注入完成后,移除 script 标签(可选)
      // script.remove();
    }
    document.head.appendChild(script);
  })
}

// 在页面加载完成后注入
window.addEventListener('load', injectCustomElement);

vite.config.ts


import { defineConfig, Plugin } from 'vite'
import path from 'path'
import react from '@vitejs/plugin-react'
export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
  // 构建配置
  build: {
    emptyOutDir: true, // 构建前清空文件夹
    target: 'esnext',
    rollupOptions: {
      // 标记为外部依赖(不打包进去)
      external: ['react', 'react-dom', 'react-router', 'react-router-dom'],
      output: {
        // 确保所有依赖内联(如果有外部依赖需额外配置)
        inlineDynamicImports: true,
        entryFileNames: 'inject.js', // 主入口文件名
        assetFileNames: '[name].[ext]', // 静态资源文件名格式(可选)
        // 定义这些依赖的全局变量名(对应 CDN 暴露的变量)
        globals: {
          react: 'React',
          'react-dom': 'ReactDOM',
          'react-router': 'ReactRouter',
          'react-router-dom': 'ReactRouterDOM',
        },
      },
    },
  },
})

image.png

打包之后由于运行在浏览器中,export 不存在,会报错

main.tsx

import customStyle from '@/styles/custom.css?raw'
import tailwindStyle from '@/styles/dist.css?raw'
import { createRoot } from 'react-dom/client'
import './styles/index.css'
import App from './App'
import { router } from './router'
import { RouterProvider } from 'react-router'

/**扩展的标签名 */
const extensionTagName = 'hang-extension'

// 定义自定义元素类
class HangExtension extends HTMLElement {
  shadowRoot!: ShadowRoot
  /**是否挂载 */
  mounted: boolean = false
  constructor() {
    super()
    this.init()
  }

  init() {
    this.addEventListener()
    this.mount()
  }

  /**挂载组件 */
  mount() {
    if (this.shadowRoot) {
      this.mounted = true
      this.style.display = 'flex'
      return
    }
    // 创建 shadow root
    this.shadowRoot = this.attachShadow({ mode: 'open' })
    createRoot(this.shadowRoot!).render(
     <RouterProvider router={router} />,
    )
    document.documentElement.appendChild(this)
    const appendCss = (textContent: string) => {
      const style = document.createElement('style')
      style.textContent = textContent
      this.shadowRoot.appendChild(style)
    }
    const stylesString = [tailwindStyle, customStyle]
    stylesString.map((css) => appendCss(css))
    this.mounted = true
    this.style.display = 'flex'
  }

  /**隐藏组件 */
  hide() {
    this.mounted = false
    this.style.display = 'none'
  }

  /**获取挂载的节点 */
  getMountedElement() {
    // 关键点:直接通过 shadowRoot 查询元素
    const appElement = this.shadowRoot.getElementById('App')
    return appElement
  }

  /**添加监听事件 */
  addEventListener(): void {
    window.addEventListener('click', (e: MouseEvent) => {
      const target = e.target as HTMLElement
      const tagName = target.tagName.toLowerCase()
      console.log(`点击的元素标签名: ${tagName}`)
      if (tagName === extensionTagName) {
        return
      }
      if (!this.mounted) {
        this.mount()
      } else {
        this.hide()
        return
      }
    })
  }

  /**设置插件位置 */
  setPluginPosition(e: MouseEvent): void {}
}

export type IApp = InstanceType<typeof HangExtension>
// 注册自定义元素
customElements.define(extensionTagName, HangExtension)
document.documentElement.appendChild(new HangExtension())

由于使用了 react-router,会导致一系列问题

解决(使用react18 react-dom-18 react-router6 react-router-dom6)

main.tsx

import customStyle from '@/styles/custom.css?raw'
import tailwindStyle from '@/styles/dist.css?raw'
import { createRoot } from 'react-dom/client'
import './styles/index.css'
import App from './App'
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'

function Routers() {
  return (
    <Router>
      <Routes>
        <Route path="/" element={<App />} />
      </Routes>
    </Router>
  )
}

/**扩展的标签名 */
const extensionTagName = 'hang-extension'

// 定义自定义元素类
class HangExtension extends HTMLElement {
  shadowRoot!: ShadowRoot
  /**是否挂载 */
  mounted: boolean = false
  constructor() {
    super()
    this.init()
  }

  init() {
    this.addEventListener()
    this.mount()
  }

  /**挂载组件 */
  mount() {
    if (this.shadowRoot) {
      this.mounted = true
      this.style.display = 'flex'
      return
    }
    // 创建 shadow root
    this.shadowRoot = this.attachShadow({ mode: 'open' })
    createRoot(this.shadowRoot!).render(<Routers />)
    document.documentElement.appendChild(this)
    const appendCss = (textContent: string) => {
      const style = document.createElement('style')
      style.textContent = textContent
      this.shadowRoot.appendChild(style)
    }
    const stylesString = [tailwindStyle, customStyle]
    stylesString.map((css) => appendCss(css))
    this.mounted = true
    this.style.display = 'flex'
  }

  /**隐藏组件 */
  hide() {
    this.mounted = false
    this.style.display = 'none'
  }

  /**获取挂载的节点 */
  getMountedElement() {
    // 关键点:直接通过 shadowRoot 查询元素
    const appElement = this.shadowRoot.getElementById('App')
    return appElement
  }

  /**添加监听事件 */
  addEventListener(): void {
    window.addEventListener('click', (e: MouseEvent) => {
      const target = e.target as HTMLElement
      const tagName = target.tagName.toLowerCase()
      console.log(`点击的元素标签名: ${tagName}`)
      if (tagName === extensionTagName) {
        return
      }
      if (!this.mounted) {
        this.mount()
      } else {
        this.hide()
        return
      }
    })
  }

  /**设置插件位置 */
  setPluginPosition(e: MouseEvent): void {}
}

export type IApp = InstanceType<typeof HangExtension>
// 注册自定义元素
customElements.define(extensionTagName, HangExtension)
document.documentElement.appendChild(new HangExtension())

cdn下载方法也记录一下

image.png

image.png

image.png

image.png

image.png

整个流程就是这样,ok

附上一张结果图

image.png

原来还有更好的解决方法

// content.js
// 在页面加载完成后注入
window.addEventListener('load', () => {
  const script = document.createElement('script');
  script.type = "module"
  // <script type="module" crossorigin src="/inject.js"></script>
  script.src = chrome.runtime.getURL('inject.js');
  script.onload = () => {
    // 注入完成后,移除 script 标签(可选)
    // script.remove();
    console.log(`%c✨%c%s`, 'color: #ff4757; font-weight: bold;', 'color: #2ed573;', '脚本注入成功');

  };
  document.head.appendChild(script);
});

script.type = "module"

这样就不用担心了!