如何在Tauri中加载包含导入语句的独立JS模块?
我基于Vite构建了一个Tauri应用,希望让用户能够加载自定义的小型JS脚本——理想状态下,用户只需要提供一个index.js作为入口,我通过扫描文件夹就能从这个文件开始加载,并且为了安全与隔离性,这些脚本最好在Web Worker环境中运行。
我尝试过动态加载脚本内容的方式,代码如下:
import { readTextFile } from '@tauri-apps/api/fs'; import { appDataDir } from '@tauri-apps/api/path'; const appDataDirPath = await appDataDir(); const workerScriptPath = `${appDataDirPath}/agents/sample/worker.js`; const scriptContent = await readTextFile(workerScriptPath); const blob = new Blob([scriptContent], { type: 'application/javascript' }); const workerUrl = URL.createObjectURL(blob); // 创建Worker实例 // const worker = new Worker(workerUrl); const worker = new Worker(workerUrl, { type: 'module' }); worker.onmessage = (event) => { logi('Message from worker:', event.data); }; worker.onerror = (event) => { loge('[SIDEBAR]', `🟥 Error in worker: ${event.message}`); loge('[SIDEBAR]', JSON.stringify(event)); }; worker.postMessage( JSON.stringify({ message: 'Hello from main script!', dataDir: appDataDirPath }) );
这种方式能正常加载单文件的index.js,但如果文件内部包含import导入语句就会失效——因为我只是把单个文件的内容传给了Worker,浏览器无法解析外部文件系统中的依赖模块。
下面是我的Worker脚本代码(它不在项目打包目录内,是文件系统中的独立文件):
// 直接导入本地文件不行 // import agentFn from "./index"; // importScripts也不适用,因为需要从非打包目录动态加载 // importScripts("./index.js") onmessage = async (event) => { const parsedEvent = JSON.parse(event.data); console.log('Message received from main script:'); console.log(parsedEvent); // 尝试动态加载外部脚本失败 // const agentFn = await import(`${parsedEvent.dataDir}agents/sample/index.js`); console.log(agentFn); postMessage('Hello, main script!'); };
请问有没有办法实现需求:将包含导入语句的外部JS文件加载到Tauri的WebView中?
可行解决方案
方案1:主进程构建模块映射,代理Worker加载
浏览器无法直接解析本地文件系统的模块路径,你可以在主进程中提前扫描脚本目录,构建完整的模块依赖映射,再通过代理Worker加载:
步骤1:扫描脚本目录并构建模块映射
编写逻辑遍历用户提供的脚本文件夹,读取所有.js文件的内容,生成一个键为模块相对路径、值为文件内容的映射对象,比如:// 示例模块映射 const moduleMap = { "./index.js": "export default function() { ... }", "./utils.js": "export const helper = () => { ... }" };步骤2:生成代理Worker脚本
创建一个代理脚本,接收主进程传递的模块映射,重写模块加载逻辑,然后加载用户的入口文件:// 主进程动态生成的代理Worker内容 const moduleMap = ${JSON.stringify(moduleMap)}; self.loadModule = async (specifier) => { if (!moduleMap[specifier]) throw new Error(`Module ${specifier} not found`); const blob = new Blob([moduleMap[specifier]], { type: 'application/javascript' }); const url = URL.createObjectURL(blob); const module = await import(url); URL.revokeObjectURL(url); return module; }; // 加载用户入口文件 const entryModule = await loadModule('./index.js'); // 这里可以根据需求调用入口模块的方法,或者绑定消息处理逻辑主进程将这个代理脚本打包成Blob,创建Worker实例。
方案2:注册Tauri自定义协议,直接访问外部模块
Tauri支持注册自定义协议,将用户脚本目录映射到WebView可访问的协议路径,这样就能用标准ES模块语法加载:
步骤1:配置自定义协议
在tauri.conf.json中添加自定义协议配置,将用户脚本目录(比如$APPDATA/agents)映射到agent://协议:{ "tauri": { "allowlist": { "protocol": { "all": true, "custom": ["agent"] } }, "protocol": { "custom": [ { "name": "agent", "scheme": "agent", "privileged": true, "fs": { "scope": ["$APPDATA/agents/**"] } } ] } } }步骤2:在Worker中使用自定义协议导入
将本地文件路径转换为自定义协议路径,直接用import加载:onmessage = async (event) => { const { dataDir } = JSON.parse(event.data); // 转换本地路径为自定义协议路径 const entryUrl = `agent://${dataDir.split('agents')[1]}/sample/index.js`; const agentModule = await import(entryUrl); console.log(agentModule); postMessage('Hello, main script!'); };这种方式让WebView通过Tauri的权限控制访问本地文件,原生支持ES模块的依赖解析。
方案3:递归解析依赖树,转换为Blob URL链
如果用户脚本的依赖层级不深,可以在主进程中递归解析所有导入依赖,将每个文件转换为Blob URL,替换原文件中的相对导入路径,最后在Worker中加载入口Blob URL:
步骤1:递归解析依赖
编写函数读取入口文件,解析其中的import语句,递归读取所有依赖文件,将每个文件的相对导入替换为对应的Blob URL。步骤2:加载入口Blob URL
主进程将处理后的入口文件Blob URL传给Worker,Worker直接import该URL,此时所有依赖已经被替换为可访问的Blob URL,浏览器能正常解析模块依赖。
内容的提问来源于stack exchange,提问作者Oscar Franco

