eIQ扩展API实战:Vue.js集成与前后端通信架构详解

📅 2026/6/17 11:53:12 ✍️ 编辑团队 👁️ 阅读次数
eIQ扩展API实战:Vue.js集成与前后端通信架构详解
1. 项目概述与核心价值如果你正在使用NXP的eIQ Toolkit进行嵌入式AI开发并且觉得标准功能在某些定制化场景下不够用那么它的扩展APIExtension API就是你必须要掌握的一把利器。这个API不是简单的脚本接口而是一个完整的、基于TypeScript/JavaScript的插件开发框架它允许你将eIQ Portal从一个“开箱即用”的工具转变为一个可以根据你的具体工作流深度定制的AI开发平台。想象一下你可以在模型验证后自动触发一个自定义的优化脚本或者为特定的硬件平台比如i.MX 93构建一个一键式模型转换和部署界面甚至集成一套复杂的模型可解释性分析工具——所有这些都可以通过扩展API来实现。本次分享的核心是聚焦于扩展API中一个极具实用价值的高级特性如何将现代化的Vue.js前端框架无缝集成到你的扩展中并建立起前端Web视图与后端扩展主进程之间稳定、高效的双向通信通道。官方文档虽然给出了骨架但很多实战中的“坑”和最佳实践并未详述。我将结合一个模拟的“模型性能分析面板”扩展案例从头拆解如何搭建这样一个项目包括项目结构设计、通信机制实现、以及如何利用Python虚拟环境执行自定义任务。无论你是想为团队内部打造专属工具还是希望深化对eIQ生态的理解这篇文章都能提供从理论到代码的完整路径。2. 扩展项目架构深度解析一个典型的eIQ扩展项目远不止是写几行JavaScript。它遵循着与VS Code扩展类似的设计哲学强调模块化、类型安全和前后端分离。理解这个架构是进行任何高级开发的前提。2.1 核心目录结构与职责官方示例给出了一个基础结构但在实际项目中我们需要一个更清晰、更易于维护的布局。以下是我在实践中总结的一种优化结构your-extension/ ├── package.json # 扩展的清单文件定义命令、菜单、视图等 ├── tsconfig.json # TypeScript编译配置 ├── webpack.config.js # 可选用于打包前端资源优化加载 ├── src/ │ ├── extension/ # 扩展后端主逻辑Node.js环境 │ │ ├── extension.ts # 扩展激活入口注册命令等 │ │ ├── workspace.ts # 自定义工作区或面板的逻辑 │ │ └── messaging/ # 后端消息处理模块 │ │ └── handler.ts # 处理来自前端的消息 │ └── frontend/ # 扩展前端应用Web视图环境 │ ├── public/ # 静态资源如index.html模板 │ ├── src/ │ │ ├── main.ts # Vue应用入口安装插件 │ │ ├── App.vue # 根组件 │ │ ├── views/ # 页面级Vue组件 │ │ ├── components/ # 可复用的UI组件 │ │ ├── composables/ # Vue 3组合式API逻辑复用 │ │ ├── stores/ # Pinia状态管理如需要 │ │ └── messaging/ # 前端消息通信模块 │ │ ├── plugin.ts # Vue插件将通信API注入组件 │ │ ├── client.ts # 封装与扩展后端的通信方法 │ │ └── types.ts # 共享的类型定义 │ └── package.json # 前端项目的依赖管理独立 └── dist/ # 编译输出目录 ├── extension.js # 编译后的扩展主文件 └── frontend/ # 打包后的前端资源为什么这样设计前后端解耦src/extension和src/frontend完全分离。前端可以使用自己独立的package.json和构建工具如Vite或Webpack享受现代前端开发的所有便利例如热重载、按需编译等。类型安全在src/frontend/src/messaging/types.ts中定义前后端通信的Channel频道名称和消息体Payload的TypeScript接口。这能确保在开发阶段就发现类型错误避免运行时问题。可维护性将消息处理逻辑单独抽离到handler.ts和client.ts使得业务逻辑与通信逻辑分离代码更清晰也便于单元测试。2.2package.json扩展的“身份证”与“菜单”这个文件是eIQ Portal识别和加载扩展的核心。除了定义扩展名称、版本、激活事件activationEvents外最重要的部分是contributes。{ name: model-analyzer, main: ./dist/extension.js, activationEvents: [onCommand:modelAnalyzer.openView], contributes: { commands: [{ command: modelAnalyzer.openView, title: 打开模型分析面板 }], menus: { editor/title: [{ command: modelAnalyzer.openView, when: activeProject ! undefined, group: navigation }] }, viewsContainers: { activitybar: [{ id: model-analyzer, title: 模型分析器, icon: ./assets/icon.svg }] }, views: { model-analyzer: [{ id: model-analyzer.view, name: 分析面板 }] } } }关键点解析activationEvents控制扩展何时被加载。使用onCommand:可以延迟加载直到用户真正点击了相关命令这能提升eIQ Portal的启动性能。menus中的when子句这是实现条件化菜单显示的关键。示例中when: activeProject ! undefined意味着只有在有项目被打开时这个菜单项才会显示。你可以利用modelExported、exportModelQuantized等上下文键Context Keys构建复杂的显示逻辑例如只在导出量化TFLite模型后才显示“Vela优化”按钮。viewsContainers与views这允许你在eIQ Portal的侧边活动栏Activity Bar添加一个全新的视图容器类似Explorer、Search并在其中创建自定义视图。这比单纯弹出一个Webview面板更集成、更专业。3. 双向通信机制从理论到稳健实现官方文档提到了IMessaging接口的概念send,receive,receiveOnce,request但把实现留给了开发者。这里我将提供一个生产环境可用的、带有错误处理和状态管理的实现方案。3.1 通信模型与挑战eIQ扩展的前端Webview和后端Extension Host运行在完全隔离的上下文中。前端是一个被沙箱化的浏览器环境后端是Node.js进程。它们之间的通信类似于iframe与父页面的通信主要依靠postMessage和事件监听。核心挑战异步性所有通信都是异步的。你需要妥善处理Promise和回调。生命周期管理Webview可能被关闭和重新打开需要清理之前注册的事件监听器防止内存泄漏。类型安全消息格式需要保持一致避免解析错误。3.2 后端Extension通信层实现在后端我们创建一个MessagingHandler类来集中管理通信。// src/extension/messaging/handler.ts import * as vscode from vscode; export type MessageHandlerT any, R any (payload: T, webview: vscode.Webview) PromiseR | R; export class MessagingHandler { private handlers: Mapstring, MessageHandler new Map(); constructor(private context: vscode.ExtensionContext) {} // 注册一个消息处理器 registerHandlerT, R(channel: string, handler: MessageHandlerT, R) { if (this.handlers.has(channel)) { console.warn(Handler for channel ${channel} is being overwritten.); } this.handlers.set(channel, handler); } // 处理来自Webview的消息 async handleWebviewMessage(webview: vscode.Webview, message: any): Promisevoid { const { channel, id, data, isRequest } message; if (!channel || !this.handlers.has(channel)) { console.error(No handler registered for channel: ${channel}); // 如果是请求需要返回错误响应 if (isRequest id) { this._sendToWebview(webview, { id, error: No handler for channel: ${channel} }); } return; } const handler this.handlers.get(channel)!; try { const result await handler(data, webview); // 只有请求才需要回复 if (isRequest id) { this._sendToWebview(webview, { id, result }); } } catch (error) { console.error(Error handling message on channel ${channel}:, error); if (isRequest id) { this._sendToWebview(webview, { id, error: (error as Error).message }); } } } // 主动发送消息到Webview非请求-响应模式 sendToWebviewT(webview: vscode.Webview, channel: string, data: T): void { this._sendToWebview(webview, { channel, data }); } private _sendToWebview(webview: vscode.Webview, message: any): void { webview.postMessage(message).then( () {/* 发送成功 */}, (err) console.error(Failed to post message to webview:, err) ); } }在扩展激活文件中使用// src/extension/extension.ts import * as vscode from vscode; import { MessagingHandler } from ./messaging/handler; export function activate(context: vscode.ExtensionContext) { const messagingHandler new MessagingHandler(context); // 注册一个命令来创建Webview面板 const disposable vscode.commands.registerCommand(modelAnalyzer.openView, () { const panel vscode.window.createWebviewPanel( modelAnalyzerView, 模型分析面板, vscode.ViewColumn.One, { enableScripts: true } // 必须启用脚本 ); // 设置HTML内容包含Vue应用 panel.webview.html getWebviewContent(context, panel.webview); // 监听来自Webview的消息 panel.webview.onDidReceiveMessage( (message) messagingHandler.handleWebviewMessage(panel.webview, message), undefined, context.subscriptions ); // 注册一个示例处理器获取当前项目信息 messagingHandler.registerHandler(getActiveProject, async () { const activeProject (global as any).eiqextension?.project?.activeProject; return { name: activeProject?.name || 无活动项目, modelType: activeProject?.activeModelCheckpoint?.type }; }); // 主动推送一个消息到前端例如项目状态变更 // setTimeout(() { // messagingHandler.sendToWebview(panel.webview, projectUpdated, { timestamp: Date.now() }); // }, 5000); }); context.subscriptions.push(disposable); }3.3 前端Vue通信层实现在前端我们创建一个Vue插件和对应的Composable以提供响应式、易用的通信API。首先定义类型// src/frontend/src/messaging/types.ts export interface RequestMessage { id: string; channel: string; data?: any; isRequest: true; } export interface ResponseMessage { id: string; result?: any; error?: string; } export interface EventMessage { channel: string; data: any; } export type IncomingMessage ResponseMessage | EventMessage;然后实现核心的通信客户端// src/frontend/src/messaging/client.ts import { ref, onUnmounted } from vue; declare const acquireVsCodeApi: any; const vscode acquireVsCodeApi(); // eIQ Portal注入的API export function useMessaging() { const eventListeners new Mapstring, SetFunction(); const pendingRequests new Mapstring, { resolve: Function; reject: Function }(); // 初始化监听来自扩展的消息 window.addEventListener(message, (event) { const message: IncomingMessage event.data; // 处理请求响应 if (id in message pendingRequests.has(message.id)) { const { resolve, reject } pendingRequests.get(message.id)!; pendingRequests.delete(message.id); if (message.error) { reject(new Error(message.error)); } else { resolve(message.result); } return; } // 处理事件消息 if (channel in message) { const listeners eventListeners.get(message.channel); if (listeners) { listeners.forEach(listener listener(message.data)); } } }); // 发送消息到扩展无回复 function sendT(channel: string, data?: T): void { vscode.postMessage({ channel, data }); } // 发送请求到扩展期待回复 function requestT, R(channel: string, data?: T): PromiseR { return new Promise((resolve, reject) { const id req_${Date.now()}_${Math.random()}; pendingRequests.set(id, { resolve, reject }); // 设置超时 const timeoutId setTimeout(() { pendingRequests.delete(id); reject(new Error(Request to channel ${channel} timed out.)); }, 30000); // 30秒超时 // 覆盖resolve/reject以清理超时 const originalResolve resolve; const originalReject reject; pendingRequests.set(id, { resolve: (value: R) { clearTimeout(timeoutId); originalResolve(value); }, reject: (err: any) { clearTimeout(timeoutId); originalReject(err); } }); vscode.postMessage({ id, channel, data, isRequest: true }); }); } // 监听来自扩展的事件 function receiveT(channel: string, callback: (data: T) void): () void { if (!eventListeners.has(channel)) { eventListeners.set(channel, new Set()); } eventListeners.get(channel)!.add(callback); // 返回一个取消监听的函数 return () { const listeners eventListeners.get(channel); if (listeners) { listeners.delete(callback); if (listeners.size 0) { eventListeners.delete(channel); } } }; } // 组件卸载时清理 onUnmounted(() { eventListeners.clear(); pendingRequests.clear(); }); return { send, request, receive }; }最后将其封装为Vue插件方便在组件中注入使用// src/frontend/src/messaging/plugin.ts import { App } from vue; import { useMessaging } from ./client; const plugin { install(app: App) { // 提供全局的 messaging 对象但更推荐在组件内使用 useMessaging() app.config.globalProperties.$messaging useMessaging(); } }; // 同时导出一个组合式函数用于在setup语法中使用 export { useMessaging }; export default plugin;在Vue应用中使用!-- src/frontend/src/App.vue -- template div button clickfetchProjectInfo获取项目信息/button p v-ifprojectInfo{{ projectInfo.name }} - {{ projectInfo.modelType }}/p p状态: {{ status }}/p /div /template script setup langts import { ref, onMounted, onUnmounted } from vue; import { useMessaging } from ./messaging/client; const { request, receive } useMessaging(); const projectInfo refany(null); const status ref(等待连接...); const fetchProjectInfo async () { try { const info await request(getActiveProject); projectInfo.value info; status.value 数据加载成功; } catch (error) { status.value 错误: ${error.message}; } }; // 监听扩展主动推送的事件 const unsubscribe receive(projectUpdated, (data) { console.log(项目已更新:, data); status.value 项目于 ${new Date(data.timestamp).toLocaleTimeString()} 更新; }); // 组件卸载时取消监听 onUnmounted(() { unsubscribe(); }); /script实操心得通信设计模式在实践中我建议将通信模式规范化。例如定义清晰的频道命名规范如data:fetchProject、event:projectUpdated、command:runScript。对于复杂的交互可以考虑在前后端共享一个状态管理如通过request同步关键状态但更推荐使用事件驱动send/receive来解耦。request模式最适合需要明确结果的操作如调用一个Python脚本并获取其输出。4. 集成Vue.js与构建部署实战将Vue.js集成到eIQ扩展中意味着你的前端部分是一个完整的现代Web应用。这带来了更好的开发体验和更强的交互能力但也引入了构建和资源加载的复杂性。4.1 构建配置与资源加载eIQ的Webview无法直接加载本地文件系统中的file://资源。你必须使用webview.asWebviewUri()方法将本地路径转换为Webview可以加载的特殊URI。前端构建以Vite为例你的前端package.json中应有构建脚本{ scripts: { dev: vite, build: vite build --outDir ../../dist/frontend, // 输出到扩展的dist目录 preview: vite preview } }运行npm run build后所有资源JS、CSS、图片会被打包到dist/frontend目录并生成入口文件如index.html和资源映射。后端生成Webview HTML在后端你需要动态生成HTML并正确引用这些打包后的资源。// 在 extension.ts 或一个独立的工具函数中 import * as path from path; import * as fs from fs; import * as vscode from vscode; function getWebviewContent(context: vscode.ExtensionContext, webview: vscode.Webview): string { // 前端资源在磁盘上的路径 const frontendDistPath path.join(context.extensionPath, dist, frontend); // 转换为Webview可用的URI const scriptUri webview.asWebviewUri(vscode.Uri.file(path.join(frontendDistPath, assets/index-xxxxxx.js))); const styleUri webview.asWebviewUri(vscode.Uri.file(path.join(frontendDistPath, assets/index-xxxxxx.css))); // 读取构建生成的index.html模板并替换资源路径 let html fs.readFileSync(path.join(frontendDistPath, index.html), utf-8); // 注意简单的字符串替换可能不健壮对于复杂情况建议使用cheerio等库 html html.replace( /(link[^]*?href)[^]*?(?)/g, $1${styleUri} ).replace( /(script[^]*?src)[^]*?(?)/g, $1${scriptUri} ); // 关键设置严格的Content Security Policy (CSP) const cspSource webview.cspSource; const cspMeta meta http-equivContent-Security-Policy contentdefault-src ${cspSource}; style-src ${cspSource} unsafe-inline; script-src ${cspSource};; // 将CSP Meta标签插入head html html.replace(/head, ${cspMeta}/head); return html; }注意事项CSP内容安全策略Webview有严格的安全限制。你必须正确设置CSP否则Vue应用可能无法运行。webview.cspSource提供了允许加载资源的源。unsafe-inline对于某些CSS是必要的但应尽量避免。如果你的Vue应用需要加载外部字体或图片也需在CSP中相应添加font-src或img-src指令。4.2 利用Python虚拟环境执行自定义任务eIQ扩展API最强大的功能之一是能够直接在你的扩展中调用与eIQ Portal同源的Python环境。这意味着你可以无缝使用TensorFlow、PyTorch、NumPy等已安装的库或者安装新的依赖来运行自定义的模型后处理、数据分析脚本。核心API使用示例假设我们要在扩展中添加一个功能使用自定义的Python脚本分析当前模型的层信息。// 在扩展后端extension.ts中 import * as vscode from vscode; // 1. 安装自定义Python包可选首次运行时进行 async function ensurePythonDependencies(context: vscode.ExtensionContext) { try { // 安装一个用于模型分析的额外包例如 netron用于模型可视化的CLI版本 await context.pythonEnvironment.installModule(netron); console.log(Python dependencies installed successfully.); } catch (error) { vscode.window.showErrorMessage(安装Python依赖失败: ${error}); // 根据错误决定是否阻止功能运行 } } // 2. 执行Python脚本 async function analyzeModelWithPython(context: vscode.ExtensionContext) { const activeProject (global as any).eiqextension?.project?.activeProject; if (!activeProject?.activeModelCheckpoint?.path) { vscode.window.showWarningMessage(没有活动的模型可供分析。); return; } const modelPath activeProject.activeModelCheckpoint.path; // 准备要传递给Python脚本的变量 const scriptVariables { model_path: modelPath, analysis_detail: layer_names // 可以传递复杂对象Javascript对象会被序列化 }; // Python脚本字符串 const pythonScript import json import sys import tensorflow as tf # 从Javascript传递的变量可以直接使用 print(f分析模型: {model_path}, filesys.stderr) try: # 加载模型这里以Keras SavedModel为例需根据实际模型类型调整 model tf.keras.models.load_model(model_path) analysis_result { model_type: type(model).__name__, layer_count: len(model.layers), layer_names: [layer.name for layer in model.layers], input_shape: model.input_shape, output_shape: model.output_shape } # 将结果打印到stdout会被Javascript捕获 print(json.dumps(analysis_result)) except Exception as e: print(fERROR: {e}, filesys.stderr) sys.exit(1) ; try { vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: 正在分析模型结构..., cancellable: false }, async (progress) { // execScript 返回一个Promise其结果为[output, error]的数组 const [output, error] await context.pythonEnvironment.execScript(pythonScript, scriptVariables); if (error) { throw new Error(Python脚本执行错误: ${error}); } const resultJson output.stdout.trim(); if (!resultJson) { throw new Error(Python脚本未输出有效结果。); } const analysisResult JSON.parse(resultJson); // 将结果发送到前端显示 // 这里假设你有一个Webview面板的引用 panel // messagingHandler.sendToWebview(panel.webview, modelAnalysisResult, analysisResult); vscode.window.showInformationMessage(模型分析完成共 ${analysisResult.layer_count} 层。); console.log(模型分析结果:, analysisResult); }); } catch (error) { vscode.window.showErrorMessage(模型分析失败: ${error.message}); console.error(Python执行异常:, error); } } // 在扩展激活时或某个命令中调用 export async function activate(context: vscode.ExtensionContext) { // ... 其他初始化代码 ... // 确保Python环境就绪 await ensurePythonDependencies(context); // 注册一个执行分析的命令 const analyzeDisposable vscode.commands.registerCommand(modelAnalyzer.runPythonAnalysis, () { analyzeModelWithPython(context); }); context.subscriptions.push(analyzeDisposable); }关键点与避坑指南错误处理execScript的调用必须用try-catch包裹。Python脚本中的错误会通过stderr或抛出异常的方式传递回来。数据传递通过variables参数传递的数据在Python脚本中会作为全局变量存在。传递复杂对象如数组、字典是可行的但要注意Python和JavaScript在数据类型如None/null,Tuple上的差异。如文档所述元组需要以字符串形式传递。性能与阻塞执行长时间运行的Python脚本会阻塞扩展主线程。对于耗时任务考虑使用vscode.window.withProgress显示进度条。在Python脚本中输出进度信息到stderr然后在前端通过消息通道进行解析和显示。如果任务非常繁重可能需要考虑在扩展中启动一个独立的Python子进程但这更复杂。环境隔离context.pythonEnvironment提供的是一个虚拟环境它派生自eIQ Portal的主环境。你在这里安装的包不会影响主环境这保证了扩展之间的独立性。5. 状态存储与项目数据操作扩展API提供了两种存储方式globalState和projectState。它们是键值对存储用于持久化扩展的状态。5.1 全局状态与项目状态globalState存储在用户级别与当前打开的项目无关。适合保存用户偏好设置、扩展的全局配置等。// 保存一个全局标记 context.globalState.update(hasShownWelcome, true); // 读取如果不存在则返回默认值false const hasShown context.globalState.get(hasShownWelcome, false);projectState与当前打开的项目绑定。项目关闭或切换时这些数据会随之保存和加载。适合保存针对特定项目的分析结果、临时配置等。// 保存当前项目的某个分析配置 context.projectState.update(lastAnalysisParams, { threshold: 0.5, method: gradcam }); // 当用户重新打开该项目时可以读取这些配置 const params context.projectState.get(lastAnalysisParams);5.2 操作项目数据集eiqextension.project.activeProject.datasetAPI 允许你以编程方式与eIQ Portal中的数据集进行交互。这在自动化数据预处理、批量修改标签或导出增强数据时非常有用。// 假设我们已经获取了活动项目 const project (global as any).eiqextension?.project?.activeProject; if (!project) return; const dataset project.dataset; // 1. 获取所有标签 const allLabels dataset.labels; console.log(项目共有 ${allLabels.length} 个标签:, allLabels.map(l l.name)); // 2. 获取训练集的所有图片 const trainImagesFilter { group: train }; const trainImages await dataset.getImages(trainImagesFilter); console.log(训练集有 ${trainImages.length} 张图片); // 3. 获取特定标签的所有标注 const catLabel allLabels.find(l l.name cat); if (catLabel) { const catAnnotationsFilter { labelId: catLabel.id }; const catAnnotations await dataset.getAnnotations(catAnnotationsFilter); console.log(找到 ${catAnnotations.length} 个‘猫’的标注); } // 4. 添加一个新标签例如用于主动学习添加一个“不确定”的标签 const newLabel await dataset.addLabel({ name: uncertain, color: #FFA500 // 橙色 }); // 5. 为一张图片添加一个新标注 const someImage trainImages[0]; if (someImage) { const newAnnotation await dataset.addAnnotation({ imageId: someImage.id, labelId: newLabel.id, boundingBox: { x: 100, y: 100, width: 50, height: 50 } // 对于物体检测 // 对于分类可能只需要 imageId 和 labelId }); } // 6. 删除一个标注例如删除置信度低的自动标注 // const annotationToDelete ...; // await dataset.deleteAnnotation(annotationToDelete.id);注意事项数据操作的安全性通过API对数据集进行的增删改操作是直接且持久化的。在进行批量操作前务必先在小范围测试或者实现“撤销”功能可以通过备份数据到projectState来实现简单的撤销。操作图像和标注是异步的记得使用await。6. 实战构建一个模型分析面板扩展现在让我们将以上所有知识点串联起来勾勒一个“模型性能分析面板”扩展的完整实现思路。这个扩展将在活动栏添加一个图标。点击后打开一个Webview面板内部是Vue.js构建的SPA。面板加载后自动通过request获取当前活动项目的信息。提供按钮触发后端的Python脚本对模型进行分析如计算FLOPs、参数量。分析过程中前端通过receive监听进度事件并更新UI。分析完成后结果以图表形式在前端展示。用户可以将分析结果保存到projectState中下次打开同一项目时自动加载。关键步骤摘要项目初始化使用yo codeVS Code扩展生成器或手动创建上述目录结构。分别初始化前端Vue Vite和后端Node.js/TypeScript项目。配置package.json定义命令、菜单、视图。设置activationEvents为onView:model-analyzer.view实现按需激活。实现后端主逻辑在extension.ts的activate函数中注册TreeDataProvider来提供视图内容如果使用树形视图或直接创建Webview面板。创建MessagingHandler实例并注册一系列消息处理器如getProjectInfo、runModelAnalysis、saveAnalysisResult。在runModelAnalysis处理器中调用context.pythonEnvironment.execScript()执行复杂的模型分析脚本并通过sendToWebview向前端发送进度和结果。构建前端Vue应用使用useMessaging()组合式函数与后端通信。创建响应式数据ref,reactive来管理UI状态加载中、分析结果、错误信息。使用ECharts或Chart.js等库将分析结果可视化。设计UI包含项目信息展示区、分析控制按钮、结果图表展示区、历史记录列表。集成与调试编写一个简单的Python分析脚本例如使用tf.keras或onnx库加载模型并提取信息。在getWebviewContent函数中正确设置HTML和资源路径。在eIQ Portal中按F12打开开发者工具如果支持在Console和Network面板中调试前端在后端使用console.log输出日志这些日志会出现在eIQ Portal的“开发者工具”控制台或宿主系统的标准输出中取决于eIQ Portal的启动方式。打包与分发将整个项目node_modules除外打包成ZIP文件。其他用户只需将其解压到%USERPROFILE%/.eiqportal/extensions/Windows或~/.eiqportal/extensions/Linux/macOS目录下重启eIQ Portal即可使用。通过这个完整的流程你不仅能够创建一个功能强大的eIQ扩展更能深刻理解如何在一个专业的桌面应用框架内集成现代前端技术栈并实现安全、高效的前后端协同。这其中的架构思想和实战技巧对于开发其他基于Electron或类似技术的插件化应用也具有很高的参考价值。