Chrome扩展Manifest V3+Vite+TS环境下,内容脚本与后台脚本共享代码的实现方案(避免ESM导入错误)
兄弟我太懂你这个坑了!当初在Manifest V3+Vite+TS的环境里折腾共享代码的时候,也踩过一模一样的ESM导入错误——内容脚本不认import语句,Vite自动抽的共享chunk直接把整个扩展干崩了。下面给你几个亲测有效的解决方案,按需选就行:
方案一:手动控制Rollup代码分割,强制内联共享代码
这个方法最直接,就是告诉Vite:别给内容脚本搞什么共享chunk,把所有依赖都直接塞到内容脚本的打包文件里,后台脚本该怎么分割怎么分割。
你只需要在vite.config.ts里配置Rollup的代码分割规则,把共享的config或者工具代码分别绑定到后台和内容脚本的chunk里:
import { defineConfig } from 'vite'; export default defineConfig({ build: { rollupOptions: { input: { background: './src/background.ts', content: './src/content.ts', }, output: { manualChunks: (id) => { // 把config和内容脚本的依赖都打包到content chunk里,不抽共享 if (id.includes('src/config.ts') || id.includes('src/content.ts')) { return 'content'; } // 后台脚本的依赖单独打包 if (id.includes('src/background.ts')) { return 'background'; } }, }, }, }, });
配置完之后,Vite会把config.ts的代码分别内联到background和content的打包文件里,不会生成共享chunk,内容脚本里自然就不会出现讨厌的import语句了。
方案二:用Vite的define全局注入纯常量配置
如果你共享的只是纯常量配置(比如api地址、版本号这种),那用Vite的define选项更简单——直接把配置注入成全局变量,两个脚本不用导入就能用。
第一步在vite.config.ts里定义全局变量:
import { defineConfig } from 'vite'; // 这里写你的共享配置 const APP_CONFIG = { apiUrl: "https://api.example.com", featureFlag: true, version: "1.0.0", }; export default defineConfig({ define: { // 转成JSON字符串,Vite会自动替换成常量 __APP_CONFIG__: JSON.stringify(APP_CONFIG), }, });
第二步在tsconfig.json里声明全局变量类型,避免TS报错:
{ "compilerOptions": { "types": ["vite/client"], "lib": ["ESNext", "DOM"] }, "include": ["src/**/*"], "exclude": ["node_modules"] }
然后在src下新建global.d.ts文件,补充类型声明:
declare const __APP_CONFIG__: { apiUrl: string; featureFlag: boolean; version: string; };
现在不管是background.ts还是content.ts,直接写__APP_CONFIG__.apiUrl就能用了,打包后Vite会把这个全局变量直接替换成具体的常量值,完全没有导入语句。
方案三:把共享代码打包成IIFE格式(适合复杂函数/工具类)
如果共享的是函数、工具类这种有逻辑的代码,那可以把共享代码单独打包成IIFE(立即执行函数表达式),挂载到全局变量上,内容脚本直接通过window访问,后台脚本正常用ESM导入。
先改vite.config.ts,给共享代码单独配置打包格式:
import { defineConfig } from 'vite'; export default defineConfig({ build: { rollupOptions: { input: { background: './src/background.ts', content: './src/content.ts', sharedUtils: './src/shared/utils.ts', }, output: { entryFileNames: '[name].js', // 给不同入口指定打包格式 format: (chunkInfo) => { // 后台脚本用ESM格式 if (chunkInfo.name === 'background') { return 'es'; } // 内容脚本和共享代码用IIFE格式 return 'iife'; }, // 共享代码打包成IIFE后,挂载到window的SharedUtils变量上 name: (chunkInfo) => { if (chunkInfo.name === 'sharedUtils') { return 'SharedUtils'; } return chunkInfo.name; }, }, }, }, });
然后在manifest.json里的内容脚本配置里,要先加载共享代码的脚本,再加载内容脚本:
"content_scripts": [ { "matches": ["<all_urls>"], "js": ["dist/sharedUtils.js", "dist/content.js"] } ]
使用的时候,content.ts里直接用window.SharedUtils.myToolFunction(),background.ts里正常import { myToolFunction } from './shared/utils.ts'就行。
内容来源于stack exchange

