最近有个开发谷歌插件的需求,由于在开发的时候遇到一些问题,在此记录一下
问题
- 使用react vite 打包报错
- 三方包最新版本没有最新的umd包
- 注入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',
},
},
},
},
})
打包之后由于运行在浏览器中,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下载方法也记录一下
整个流程就是这样,ok
附上一张结果图
原来还有更好的解决方法
// 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"
这样就不用担心了!