我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

从零到发布:IntelliJ插件Typing Novel的开发全记录

从零到发布:IntelliJ插件Typing Novel的开发全记录 也不知道算不算个典型需求——作为一个常年泡在IDE里写代码、偶尔还想写点短篇的人我总觉得编辑器缺了点“写作氛围”。代码编辑器再强它默认也是为编码设计的没有打字机那一下清脆的反馈没有字符落定后的轻动画也没有“今天写了多少字”这种盯着涨的目标感。于是就有了Typing Novel这个IntelliJ插件项目不搞什么大而全的写作工具就做三件事——打字音效、字符落点动画、状态栏实时字数统计外加一个每日目标提醒。这篇文章把整个开发流程从创建工程到最终发布JetBrains Marketplace完整走一遍包括工程配置里那些文档不写的取舍、调试沙箱里踩过的坑以及发布前必须检查的兼容性细节。适合对插件开发有兴趣、想拿一个真实项目练手的读者也适合已经写过基础插件、但还没完整走通发布链路的人。1. 为什么要在IDE里做“打字机”Typing Novel的需求定位与后续技术选型的源头先花点时间把需求说清楚。IntelliJ IDEA 是一个极其成熟的IDE但你不可能要求它天生适合每一类场景。写小说、写博客草稿、记录接口文档的人在编辑器里停留的时间一点不比写代码短而IDE默认没有任何“写作反馈”机制。Typing Novel的定位就是一个轻量氛围插件它不改排版、不碰Markdown渲染、不做AI联想只做一件事——让每一次按键在视觉和听觉上都有回应让写作进度能持续积累。拆开说就是三个功能点打字音效每次输入一个可读字符时播放一个短促的“咔嗒”声模拟机械键盘/打字机的反馈。这需要拦截全局输入事件但不能影响原有输入逻辑。字符落点动画在光标落点位置绘制一个短暂扩散/淡出的光点目标是让视觉上感到“刚刚触发了输入”而不是光标本身闪烁。这要求在每个Editor上挂一个自定义painter。状态栏字数统计与每日目标状态栏显示当前编辑器的字符数和词数并支持设置每日目标达到目标后用通知提醒。这涉及状态栏扩展点和文档监听。为什么不做成独立编辑器因为桌面上已经有一个能多标签、能对比、能高亮、能管理项目文件的现成容器了用户不需要多一个写作软件。基于IntelliJ Plugin SDK做继承IDE的能力自己只写差异化部分这是产品定位上的理性选择。这个定位直接决定了后续技术选型使用IntelliJ Platform的扩展点机制而非全面侵入IDE输入处理沿用TypedActionHandler包装模式而非重新实现按键分发状态栏走StatusBarWidgetFactory设置持久化用PropertiesComponent。目标用户包括三种人想在IDE里专心写文档的程序员、对键盘音效有执念的创作者、还有想学插件开发但需要一个“小而完整”项目的开发者。整个项目结构我用的是Java Gradle没有碰Kotlin。不是Kotlin不好而是对于只做少量界面、核心逻辑集中在输入管道和状态栏的项目Java的调试栈更容易直读也方便后面交给不熟悉Kotlin的同事维护。这个选择在后面调试过程中帮了很大忙。2. 工程搭建第一步build.gradle里的JDK、SDK类型与JBR三件套新建IntelliJ插件项目现在最稳妥的方式是直接使用官方Gradle插件org.jetbrains.intellij而不是老的Plugin DevKit。Plugin DevKit调试时需要手动管理SDK路径Gradle方式会把SDK下载、沙箱创建、打包全部自动化。如果你用的是IntelliJ IDEA 2023.3及以上版本创建项目时直接在New Project里选“IntelliJ Platform Plugin”模板即可。但无论用不用模板最终都要检查三个核心配置JDK版本、SDK类型、JBR配置。我把实际用的build.gradle列在下面plugins { id java id org.jetbrains.intellij version 1.17.2 } group com.typingnovel version 1.0.0 repositories { mavenCentral() } dependencies { testImplementation junit:junit:4.13.2 } intellij { pluginName.set(Typing Novel) type.set(IC) version.set(2023.3.6) updateSinceUntilBuild.set(false) } patchPluginXml { sinceBuild.set(231) untilBuild.set(243.*) } java { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } runIde { jvmArgs -Xmx2G }逐个解释这几个配置的含义因为它们背后都是坑。JDK版本选17而不是21。IntelliJ Platform从2020.1开始官方要求JDK 11从2022.2开始推荐17到了2023.x版本默认JBRJetBrains Runtime已经切换到17。开发插件时sourceCompatibility/targetCompatibility必须编译为17字节码。如果本地默认JDK是21编译通常也能通过但运行时可能因为字节码版本不匹配报UnsupportedClassVersionError。图省事的话直接在IDE里把项目的SDK指向JBR 17即可本地IDEA自带的那个JBR就满足要求。SDK类型选IC而不是IU。type.set(IC)表示基于IntelliJ IDEA Community Edition构建插件不需要商业版专属功能时都该选这个因为Community的SDK下载体积小且不会依赖Ultimate专属的库。如果插件里实际用到了com.intellij.modules.ultimate等依赖type要改成IU否则运行时会提示找不到类。Typing Novel只用到了平台基础能力IC就够了。version选2023.3.6而不是追最新。Gradle插件会根据这个版本去JetBrains仓库下载对应版本的IDE作为编译和沙箱运行环境。版本越新下载包越大兼容性测试成本也越高。对个人项目来说选一个你本地主力IDE差不太多的稳定版本最舒服。sinceBuild设为231对应2023.1untilBuild设为243.*表示能兼容到2024.3系列。范围不要拉得太宽否则后续JDK版本接口变动会带来兼容测试压力。updateSinceUntilBuild.set(false)是一个容易忽略的细节。默认情况下Gradle插件会根据选择的SDK版本自动帮你填sinceBuild/untilBuild但自动填充的untilBuild往往太窄比如会限定到当前SDK的build号。实际发布插件时我们希望有一个可控的兼容范围所以手动关掉自动更新在patchPluginXml里精确控制。工程创建完成后还要确认目录结构。标准结构是src/main/java放源代码src/main/resources放META-INF/plugin.xml和其他资源文件。如果用了模板IDE通常会生成一个java目录没有的话手动建即可。3. plugin.xml与监听器注册插件“身份证”和扩展点机制详解在开始写功能代码之前必须先把META-INF/plugin.xml搞清楚。这个文件是插件的身份证里面声明了插件的唯一ID、名称、依赖、扩展点和动作。IntelliJ Platform从2020年之后开始推广注解方式注册Action但plugin.xml仍然承担核心的扩展点声明。两者各有适用场景现在Typing Novel里两个都用上了。先看完整文件idea-plugin idcom.typingnovel/id nameTyping Novel/name vendorTypingNovel Dev/vendor description![CDATA[ Typing Novel 是为IDE写作场景设计的轻量反馈插件。 br/- 打字机风格音效 br/- 字符落点动画 br/- 状态栏实时字数统计与每日目标提醒 ]]/description dependscom.intellij.modules.platform/depends dependscom.intellij.modules.lang/depends applicationListeners listener classcom.typingnovel.listener.EditorLifecycleListener topiccom.intellij.openapi.editor.event.EditorFactoryListener/ /applicationListeners extensions defaultExtensionNscom.intellij postStartupActivity implementationcom.typingnovel.startup.TypingNovelStartupActivity/ statusBarWidgetFactory idtypingnovel.wordcount implementationcom.typingnovel.statusbar.WordCountStatusBarWidgetFactory/ /extensions actions action idTypingNovel.SetDailyTarget classcom.typingnovel.action.SetDailyTargetAction textTyping Novel: 设置每日目标字数 description设置每日写作目标字数 add-to-group group-idEditorPopupMenu anchorlast/ /action /actions /idea-plugin逐项说明。插件的唯一ID不能乱改。com.typingnovel是发布到Marketplace后的全局标识一旦有用户安装这个ID就不能轻易换否则会被当作另一个插件。ID建议使用反域名格式避免和别人冲突。applicationListeners用来注册全局级别的编辑器生命周期监听。EditorFactoryListener能感知所有Editor的创建和销毁这正好满足两个需求在Editor创建时挂上painter和文档监听在销毁时释放资源。IDEA本身支持在代码里注册多个监听器但通过plugin.xml声明可以由平台统一管理生命周期不容易泄漏。extensions段落里注册了两个扩展点。postStartupActivity是平台启动完成后的回调适合做一些一次性的全局接管操作这里用来替换全局输入处理器。statusBarWidgetFactory则是状态栏挂件的官方扩展点IDEA会在适当的时候调用工厂方法创建widget。关于新旧注册方式的取舍可以参考这个对照注册方式适用场景优点缺点plugin.xml监听器、扩展点、需要稳定的全局配置生命周期由平台管理版本升级友好配置与代码分离改一处要重跑RequiresAnnotation单个Action、局部扩展代码就近少写XMLSDK版本兼容性需自查Debug时不易动态调整以Typing Novel为例Action用了actions而非注解核心原因是想在EditorPopupMenu里精确控制菜单位置。注解虽然能声明Action类但菜单分组和位置信息依然需要额外的元数据支持反而更绕。这里有一个值得一提的版本兼容性问题。早期版本的statusBarWidgetFactory扩展点和现在的接口签名略有差别在较新SDK中实现工厂类时部分接口方法已提供默认实现。如果运行时报AbstractMethodError第一反应不应该是改代码而是检查SDK版本和接口方法是否匹配。4. 核心功能一输入事件拦截、打字机音效与字符动画的完整实现这个章节是插件的“心脏”。三个功能里音效和动画都依赖同一个前提能拿到每次用户输入的可打印字符事件。4.1 全局输入管道的接管TypedActionHandler包装IntelliJ Platform把键盘字符输入统一封装在TypedAction里它持有当前生效的TypedActionHandler。做法是拿到平台现有的handler包装一层新handler先调用原逻辑保证输入不被破坏再触发自定义反馈。public class TypingHandler implements TypedActionHandler { private final TypedActionHandler originalHandler; public TypingHandler(TypedActionHandler originalHandler) { this.originalHandler originalHandler; } Override public void execute(NotNull Editor editor, char charTyped, NotNull KeyEvent keyEvent) { if (originalHandler ! null) { originalHandler.execute(editor, charTyped, keyEvent); } TypingSoundPlayer.play(); TypingEffectManager.getInstance().triggerEffect(editor, editor.getCaretModel().getCurrentCaret()); } }安装时机选在postStartupActivity里最稳妥因为平台基础服务这时已经全部初始化完毕。替换时一定要先判断是不是已经包装过避免重复嵌套。public class TypingNovelStartupActivity implements Runnable { Override public void run() { TypedAction typedAction TypedAction.getInstance(); if (!(typedAction.getHandler() instanceof TypingHandler)) { typedAction.setupHandler(new TypingHandler(typedAction.getHandler())); } } }写到这里提醒一句不要试图替换KeyHandler或者更低层的AWT Event Dispatch。原因很简单输入法组合场景尤其中文拼音输入会绕过很多底层事件直接拦截会造成字母丢失。TypedAction的抽象层次刚好在“最终可打印字符”这一级既覆盖真实输入又不和输入法冲突。4.2 音效模块Java Sound的Clip池实现Java Sound足以播放短按键音。最初版本我直接用MediaPlayer加载音频文件每次按键new一个Player结果打字稍微快点就出现声音叠音和明显卡顿。问题不在于音效文件本身而在于创建播放器的开销和并发控制。正确姿势是预加载几个Clip组成一个小池子轮流使用。每个Clip是一次性加载进内存的音效片段播放时从暂停位置恢复即可。public class TypingSoundPlayer { private static final int MAX_PLAYERS 4; private static final ListClip POOL new ArrayList(); private static int index 0; private static boolean available false; public static void init() { try { for (int i 0; i MAX_PLAYERS; i) { InputStream is TypingSoundPlayer.class.getResourceAsStream(/sounds/clack.wav); if (is null) return; try (AudioInputStream ais AudioSystem.getAudioInputStream(is)) { Clip clip AudioSystem.getClip(); clip.open(ais); POOL.add(clip); } } available true; } catch (Exception ignored) { available false; } } public static void play() { if (!available || POOL.isEmpty()) return; Clip clip POOL.get(index); index (index 1) % POOL.size(); if (clip.isRunning()) clip.stop(); clip.setFramePosition(0); clip.start(); } }设计细节有两个。一是Clip数量取4太少时快速连打会掐尾音太多则浪费内存二是每次播放前setFramePosition(0)回到开头否则第二次按下去播的是停顿后的后半段。音效文件我用的是一段约30ms的短“咔嗒”声WAV格式采样率44100或22050都可以。文件放在src/main/resources/sounds/下。4.3 落点动画EditorPainter与轻量绘制状态IntelliJ平台的Editor绘制层支持注册自定义painter。通过EditorEx.registerEditorPainter把painter挂到编辑器实例上之后每当Editor需要重绘时都会回调painter的paint方法。我们的painter不对全屏做复杂渲染只记录最近一次触发位置并绘制一个短暂光点。public class TypingEffectPainter implements EditorPainter { private final Editor editor; private Point lastPoint; private long lastTriggerAt -1; public TypingEffectPainter(Editor editor) { this.editor editor; } public void triggerAt(Caret caret) { LogicalPosition logicalPosition caret.getLogicalPosition(); lastPoint editor.logicalPositionToXY(logicalPosition); lastTriggerAt System.currentTimeMillis(); editor.getComponent().repaint(editor.getScrollingModel().getVisibleArea()); } Override public void paint(Editor hostEditor, NotNull Graphics g, NotNull Rectangle targetRegion) { if (lastPoint null || lastTriggerAt 0) return; int elapsed (int) (System.currentTimeMillis() - lastTriggerAt); if (elapsed 180) { lastPoint null; return; } float progress elapsed / 180f; int alpha (int) (255 * (1 - progress)); int radius (int) (4 6 * progress); Graphics2D g2 (Graphics2D) g.create(); g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha / 255f)); g2.setColor(new Color(0, 150, 255)); g2.fillOval(lastPoint.x - radius / 2, lastPoint.y - radius / 2, radius, radius); g2.dispose(); } }效果上是一个约180ms内扩散并淡出的蓝色圆点。这个时长是调出来的短了会闪得突兀长了会有拖影感180ms刚好是视觉残留与反馈速度的平衡点。4.4 页面生命周期绑定与性能红线光有handler和painter类还不够必须在Editor创建时把它们挂到对应实例上并在释放时清理。EditorLifecycleListener承担了这项工作。public class EditorLifecycleListener implements EditorFactoryListener { Override public void editorCreated(NotNull EditorFactoryEvent event) { Editor editor event.getEditor(); if (editor.getProject() null) return; if (editor instanceof EditorEx editorEx) { TypingEffectPainter painter new TypingEffectPainter(editor); editorEx.registerEditorPainter(painter); TypingEffectManager.getInstance().bind(editor, painter); } } Override public void editorReleased(NotNull EditorFactoryEvent event) { TypingEffectManager.getInstance().unbind(event.getEditor()); } }性能红线只有一条不要在EDT事件分发线程上做任何重量级操作。音效播放是异步的painter里只做内存计算和绘制DocumentListener回调里不要做全文遍历计算字数这个在下一章细说。初次版本我在每个documentChanged里直接统计整个文档字符数大文件里输入有明显卡顿。后来改成只在状态栏刷新时异步计算这个问题才解决。另外注意logicalPositionToXY返回的是坐标叠加滚动后会变化。为了简单动画效果直接基于可视区坐标绘制不需要考虑内部坐标和视口偏移的换算。如果做更复杂的行级动画建议用VisualPositionToXY和logicalPositionToXY组合处理。5. 核心功能二状态栏实时字数统计与每日目标提醒的实现音效和动画解决“打字氛围”字数统计解决“写作目标感”。这一章说状态栏widget的实现顺带带上目标设置与提醒。5.1 状态栏Widget的注册与展示现版本IntelliJ平台推荐通过StatusBarWidgetFactory注册状态栏组件。工厂本身作为扩展点在plugin.xml里声明IDE会为每个project调用工厂的createWidget方法将返回的widget挂到状态栏。public class WordCountStatusBarWidgetFactory implements StatusBarWidgetFactory { Override public NotNull String getId() { return typingnovel.wordcount; } Override public NotNull StatusBarWidget createWidget(NotNull Project project) { return new WordCountWidget(project); } Override public void disposeWidget(NotNull StatusBarWidget widget) { // 无额外资源需要释放 } }WordCountWidget实现CustomStatusBarWidget接口核心是一个JLabel。refresh方法读取当前活动编辑器并刷新显示文案。public class WordCountWidget implements CustomStatusBarWidget { private final Project project; private final JLabel label new JLabel(); private int dailyTarget 0; public WordCountWidget(Project project) { this.project project; dailyTarget PropertiesComponent.getInstance(project).getInt(typingnovel.dailyTarget, 0); refresh(); } public void refresh() { Editor editor FileEditorManager.getInstance(project).getSelectedTextEditor(); if (editor null) { label.setText(Typing Novel: --); return; } String text editor.getDocument().getText(); int chars removeWhitespace(text).length(); int words countWords(text); String target dailyTarget 0 ? / 目标 dailyTarget : ; label.setText(Typing Novel: chars 字 words 词 target); } private String removeWhitespace(String text) { return text.replaceAll(\\s, ); } private int countWords(String text) { String trimmed text.trim(); if (trimmed.isEmpty()) return 0; return trimmed.split(\\s).length; } Override public JComponent getComponent() { return label; } Override public String ID() { return typingnovel.wordcount; } }这里特意用了两个不同的统计口径中文字符计数剔除所有空白符适合包含中文的写作内容词数按空白分组适合英文场景。两者并存能照顾大多数写作需求。5.2 实时刷新链路DocumentListener到状态栏在Editor创建时注册DocumentListener当文档内容变化时把“刷新状态栏”的任务抛给对应project。注意不能在文档监听回调里直接做全量统计因为文档变更事件在EDT上同步执行大文件会拖慢输入响应。editor.getDocument().addDocumentListener(new DocumentListener() { Override public void documentChanged(NotNull DocumentChangeEvent event) { Project project editor.getProject(); if (project null) return; StatusBar statusBar WindowManager.getInstance().getStatusBar(project); if (statusBar null) return; WordCountWidget widget (WordCountWidget) statusBar.getWidget(typingnovel.wordcount); if (widget ! null) { widget.refresh(); } } });如果只想让状态栏刷新足够快同时又不想阻塞输入可以用DocumentBulkUpdateListener和批量更新处理。不过对于Typing Novel而言widget.refresh里做的是原子统计状态栏label组件更新大众IDEA状态栏刷新本身有批量机制实测下来中等大小的文件不会有可见卡顿。前提是不要在这个回调里做日志、持久化等额外操作。5.3 每日目标持久化与通知提醒每日目标用PropertiesComponent持久化就够了不引入第三方配置框架。设置入口是一个右键菜单Action弹输入对话框写入目标值。public class SetDailyTargetAction extends AnAction { Override public void actionPerformed(NotNull AnActionEvent e) { Project project e.getProject(); if (project null) return; String input Messages.showInputDialog(project, 输入每日目标字数, Typing Novel, Messages.getQuestionIcon()); if (input null) return; int target; try { target Integer.parseInt(input.trim()); } catch (NumberFormatException ex) { NotificationGroupManager.getInstance() .getNotificationGroup(Typing Novel) .createNotification(请输入合法数字, NotificationType.ERROR) .notify(project); return; } PropertiesComponent.getInstance(project).setValue(typingnovel.dailyTarget, target); StatusBar statusBar WindowManager.getInstance().getStatusBar(project); WordCountWidget widget statusBar null ? null : (WordCountWidget) statusBar.getWidget(typingnovel.wordcount); if (widget ! null) widget.refresh(); } }达到目标后的通知判断放在refresh里用一个boolean字段防止同一个目标被重复提醒。只提醒一次的策略是刻意为之——写作场景下反复弹通知比不弹通知更让人烦躁。这个版本没有做“每日零点重新清零”的复杂逻辑而是把提醒设计为“达到目标即通知一次”。如果后续有需求可以在目标超过时记录日期第二天首次打开时重置状态。6. 从runIde沙箱到JetBrains Marketplace发布完整调试与上架链路写完代码只是开始真正折磨人的是“本地跑得动沙箱不生效发布没审核过”。这一章完整走一遍调试和发布链路。6.1 runIde与沙箱调试runIde是Gradle插件提供的关键任务。运行它时插件会被自动打入一个全新的IDEA实例这个实例的配置目录与正常工作IDEA完全隔离默认在项目的build/idea-sandbox下。这个隔离设计非常有用——你可以放心验证插件的安装、升级、卸载不会污染主力IDE配置。调试插件和普通应用调试一样在代码里打断点然后以Debug模式运行runIde。当沙箱IDEA启动后执行对应操作断点就会命中。唯一需要留意的是沙箱启动本身也会加载一批平台类如果断点打在postStartupActivity这类启动回调里调试面板会先看到大量平台内部类的加载过程不要被干扰。沙箱的日志位置是build/idea-sandbox/system/log/idea.log。插件不生效时第一件事就是开这个日志搜索插件ID比如搜com.typingnovel。日志会直接告诉你扩展点加载失败、类找不到、还是版本不兼容。6.2 插件不生效的典型原因排查链路排错时我习惯按顺序检查下面几项比瞎猜高效得多插件是否进入沙箱查看沙箱的plugins目录确认Typing Novel文件夹和lib下的jar存在。不存在说明打包或配置有问题。扩展点是否被加载日志中搜com.typingnovel如果看到extension point相关错误通常是plugin.xml里类路径写错或依赖缺失。是否触发since/untilBuild限制日志会提示incompatible since build此时把patchPluginXml里的范围放宽一个版本段再试。代码是否真的执行在TypingNovelStartupActivity.run第一行打上System.out.println(Typing Novel installed)启动沙箱后看控制台输出。没有输出说明类根本没被实例化问题在注册有输出但功能不生效问题才在业务代码。这套链路基本能定位90%以上的插件加载问题。剩下10%往往是因为改了plugin.xml但忘记重新运行runIde导致沙箱加载的是旧配置。Gradle任务不会自动重新构建插件的XML资源需要先buildPlugin或至少执行build再跑runIde。6.3 buildPlugin打包发布前先本地打包。Gradle面板里找到intellij buildPlugin执行完成后产物在build/distributions/TypingNovel-1.0.0.zip。这个zip就是可上架的插件包。打包之前还有几个小动作要做确认plugin.xml里的description包含清晰的插件功能描述Marketplace审核时会读取这段描述。确认版本号格式正确version 1.0.0发布后版本号会作为Marketplace上的版本标识。在patchPluginXml里加上changeNotes比如![CDATA[第一版发布。]]。changeNotes会在更新日志里展示空内容容易被审核驳回。6.4 上架JetBrains Marketplace走到这一步插件才真正成为“所有人可安装的公开插件”。过程其实比想象中简单登录 JetBrains Marketplace 用JetBrains账号即可。在首页点“Add Plugin”填写插件名称这个名称必须与plugin.xml里的name一致。上传build/distributions下的zip包。填写详细的描述、截图、更新日志。截图建议至少一张状态栏widget效果图、一张动画演示图。提交后等待审核。个人插件的审核一般几小时到一两天内容通常包括功能是否合规、描述是否与行为一致、是否包含恶意代码。审核通过后插件会在官方Plugin Repository中公开用户可以直接在IDEA插件市场搜索到。之后每次更新版本只需要在Marketplace的管理后台重新上传新zip填写changelog再次提交审核即可。有一个容易忽视的点Marketplace要求插件ID在平台上唯一。如果你的plugin.xml里ID和别人重复上传阶段就会报错。所以提前在Marketplace搜索一下com.typingnovel是否已被占用是稳妥选择。6.5 兼容性维护和版本更新经验插件发布之后兼容性维护比开发更考验耐心。IntelliJ平台半年一个大版本有些API会标记废弃有些会被移除。我自己的维护策略是设置一个合理的兼容范围不盲目追求支持最新版本。sinceBuild在231、untilBuild在243的范围内可以覆盖2023.1到2024.3的大多数IDEA足够日常使用。每个新IDEA版本发布后在沙箱中运行一次runIde用最新SDK版本验证主要功能。如果发现某个API被废弃优先改用官方推荐的新API而不是临时patch。用户如果反馈插件不加载先让他提供Help Collect Debug Information的日志绝大多数问题都能在idea.log里找到原因。版本号遵循语义化版本小修复用1.0.x新增功能用1.1.0。Marketplace上的更新说明写得越具体用户越信任。这里顺便提一个技巧发布正式版之前可以先把插件包发给几个同事或朋友让他们用不同版本号的IDEA手动安装试一下。因为沙箱环境永远是“全新的”而真实用户环境可能装了各种插件、改过好多配置兼容性差异只有真实验证才能暴露。最终把一个IDE插件从想法变成Marketplace上能搜到的产品并没有想象中那么复杂但中间每层都藏着大量“文档之外的知识”。我这趟做Typing Novel下来最大的体会是插件的核心价值未必需要多复杂清楚划定边界、把反馈链路做顺、调试发布链路跑通就已经是一个完整且有用的项目。如果后续你想在这条路上继续深入可以尝试给Typing Novel增加ToolWindow面板、主题联动的音效配置、或者云同步每日目标这些方向的扩展难度都不大但能把插件的完成度再拉高一个台阶。
返回列表