Unity插件开发框架:从零构建高效编辑器工具集 1. 项目概述为什么Unity开发者需要自己的插件开发框架如果你在Unity社区里混迹过一段时间肯定会发现一个现象很多资深的工具开发者或者技术美术手里都有一套自己“攒”出来的插件开发工具集。这套东西可能没有统一的名字但里面通常包含了快速创建编辑器窗口、管理资产、处理序列化、以及打包发布的一整套流程。这就是我们常说的“插件开发框架”。它不是一个现成的、从Asset Store下载的插件而是一套用于高效、规范地“生产”插件的内部规范和代码库。为什么这很重要想象一下你接到一个需求为项目组做一个批量重命名场景中物体的工具。没有框架你的流程可能是打开Unity新建一个Editor文件夹写一个继承自EditorWindow的类然后开始吭哧吭哧地画UI、写逻辑、处理撤销操作、考虑异常情况。做完之后过两周美术又提需求要一个批量修改材质球属性的工具。你又得重复一遍这个过程大量的样板代码Boilerplate Code——窗口初始化、菜单项注册、GUI布局、事件处理——被再次编写。这不仅效率低下而且代码风格不一后期维护和团队协作都是噩梦。一个成熟的插件开发框架就是为了解决这些痛点而生的。它的核心价值在于标准化和提效。通过将通用的、重复性的工作抽象成可复用的模块开发者可以将精力集中在实现插件的核心业务逻辑上。无论是做一个简单的编辑器扩展还是一个复杂的、带有可视化节点编辑器的系统比如类似NodeCanvas、Koreographer那样的插件框架都能提供坚实的基础支持。从网络热词中频繁出现的“Unity编辑器物体批量添加组件”、“Unity 场景修改记录插件”、“Unity 创建表格插件”等需求来看社区对高效、可靠的编辑器工具需求旺盛而一个良好的框架正是满足这些需求的“生产力倍增器”。2. 框架核心设计构建可扩展的编辑器工具基石2.1 架构分层与职责划分一个健壮的插件框架不应是一团乱麻的实用方法集合而应有清晰的分层结构。在我的实践中通常会将其分为四层核心基础设施层这是框架的根基。主要包含序列化与持久化处理自定义数据的保存与加载。不仅要支持Unity自带的JsonUtility或Newtonsoft.Json更要妥善处理ScriptableObject资产、编辑器偏好EditorPrefs、以及项目设置存储在ProjectSettings文件夹的读写。一个常见的技巧是使用[System.Serializable]的结构体或类来定义数据模型并通过一个中心化的SettingsManager单例来管理。事件与消息总线插件内部各模块之间需要解耦通信。一个轻量级的消息系统或观察者模式实现至关重要。例如当资产数据库更新后可以发布一个AssetsRefreshed事件所有依赖此数据的UI窗口自动刷新无需持有相互的引用。通用扩展方法提供大量静态扩展方法例如对Rect、GUIStyle、SerializedProperty、GameObject等常用类型的快捷操作。这是提升编码体验最直接的一环。编辑器UI框架层这是与开发者交互最频繁的部分。目标是抽象化Unity IMGUI或UIElements的复杂性与样板代码。窗口基类封装一个BaseEditorWindow自动处理窗口的打开、关闭、布局保存使用EditorWindow.GetWindow的utility参数和EditorPrefs、以及通用的工具栏绘制。控件库将常用的、复杂的UI控件封装成易于调用的方法。例如一个增强的ObjectField不仅支持拖拽还能显示资产预览小图一个可折叠、带搜索功能的列表视图一个模仿Inspector风格的属性绘制器用于自动绘制自定义数据类的字段。布局系统提供流式布局、网格布局等辅助方法避免手动计算Rect的繁琐和容易出错。这对于制作复杂的工具窗口尤为重要。资产与项目管理层专门处理与Unity项目资产、场景相关的操作。资产遍历与筛选提供高性能的方法来遍历项目中的特定类型资产如所有材质球、预制体并支持基于名称、标签、自定义属性的复杂筛选。这里需要利用AssetDatabaseAPI但要注意性能对于大型项目可能需要异步或分帧处理。场景对象操作封装对场景中GameObject的批量操作如查找、修改、创建父子关系等并确保正确处理撤销Undo.RecordObject和场景脏标记EditorSceneManager.MarkSceneDirty。依赖关系分析实现简单的资产引用查找功能这对于制作资源清理工具非常有用。构建与分发层解决插件开发“最后一公里”的问题。自动化打包通过Editor脚本一键将插件代码、资源、文档打包成标准的.unitypackage文件。这包括处理asmdef程序集定义文件以确保依赖正确以及生成版本号。安装与更新设计一套简单的机制用于检测新版本和提供更新指引。对于内部框架这可能只是一个版本号对比和文档链接。2.2 关键设计模式的应用在设计框架时熟练运用设计模式能让代码更灵活。最常用的几种包括单例模式用于管理全局状态如框架配置、资源管理器。但需谨慎使用避免变成“上帝对象”。通常配合LazyT实现线程安全的延迟初始化。观察者模式如前所述用于模块间松耦合通信。C#自带的事件event和委托delegate就是其天然实现。策略模式当你需要为某个操作如“导出数据”提供多种算法导出为JSON、CSV、二进制时定义一个接口让不同的具体策略类去实现上下文类负责调用。这使得框架易于扩展新的功能。工厂模式用于创建复杂的UI控件或数据处理对象。例如根据资产类型的不同工厂返回不同的“资产处理器”实例。注意不要为了用模式而用模式。框架设计的首要原则是“实用”和“清晰”。如果一个简单的静态方法就能解决问题就不要过度设计成复杂的模式。框架的API应该直观、易用让使用者一眼就知道该怎么调用。3. 实战从零搭建一个简易插件框架理论说再多不如动手做一遍。下面我们聚焦于最核心的编辑器UI框架层搭建一个能快速创建工具窗口的迷你框架。3.1 第一步定义核心数据与配置首先我们需要一个地方来存储框架和各个插件的配置。ScriptableObject是最佳选择因为它可以作为资产保存在项目中。// FrameworkSettings.cs using UnityEngine; using UnityEditor; [CreateAssetMenu(fileName FrameworkSettings, menuName MyPluginFramework/Settings)] public class FrameworkSettings : ScriptableObject { private static FrameworkSettings _instance; public static FrameworkSettings Instance { get { if (_instance null) { // 尝试从项目加载如果不存在则创建默认 var guids AssetDatabase.FindAssets(t:FrameworkSettings); if (guids.Length 0) { string path AssetDatabase.GUIDToAssetPath(guids[0]); _instance AssetDatabase.LoadAssetAtPathFrameworkSettings(path); } else { Debug.LogWarning(FrameworkSettings not found. Creating default.); _instance CreateInstanceFrameworkSettings(); // 通常这里会初始化默认值 } } return _instance; } } // 框架全局配置 public string FrameworkVersion 1.0.0; public bool EnableDebugLog true; public Color PrimaryColor new Color(0.2f, 0.6f, 1.0f); // 框架主题色 // 可以在这里添加更多配置如默认资源路径、快捷键映射等 }这个单例模式提供了全局访问点。我们还需要一个管理器来初始化框架// FrameworkInitializer.cs using UnityEditor; using UnityEngine; [InitializeOnLoad] public static class FrameworkInitializer { static FrameworkInitializer() { // 当Unity编辑器加载或重载脚本时此静态构造函数会被调用 EditorApplication.delayCall Initialize; } private static void Initialize() { // 强制访问一次Instance确保设置文件被加载或创建 var settings FrameworkSettings.Instance; if (FrameworkSettings.Instance.EnableDebugLog) { Debug.Log($[MyPluginFramework] Initialized. Version: {settings.FrameworkVersion}); } // 可以在这里注册全局事件、初始化服务等 } }3.2 第二步创建强大的窗口基类这是框架的“重头戏”。我们将创建一个BaseEditorWindow它解决了原生EditorWindow的许多不便。// BaseEditorWindow.cs using UnityEditor; using UnityEngine; using System; using System.Collections.Generic; public class BaseEditorWindow : EditorWindow { // 每个窗口实例的唯一标识符用于存储窗口布局 protected virtual string WindowId this.GetType().FullName; // 窗口的默认大小 protected virtual Vector2 DefaultSize new Vector2(800, 600); // 存储的窗口位置和大小 private Rect _savedWindowRect; // 用于流式布局的当前Y位置 protected float CurrentLayoutY { get; set; } // 左边距和右边距 protected const float LeftMargin 10f; protected const float RightMargin 10f; protected const float ElementSpacing 5f; // 初始化窗口 protected virtual void OnEnable() { // 加载保存的窗口位置和大小 LoadWindowState(); // 设置最小尺寸 minSize new Vector2(DefaultSize.x * 0.5f, DefaultSize.y * 0.5f); // 订阅编辑器更新用于实现自定义的Repaint逻辑如动画 EditorApplication.update OnEditorUpdate; } protected virtual void OnDisable() { // 保存窗口状态 SaveWindowState(); // 取消订阅 EditorApplication.update - OnEditorUpdate; } // 加载窗口状态 private void LoadWindowState() { string key $MyPluginFramework_WindowRect_{WindowId}; if (EditorPrefs.HasKey(key)) { string rectStr EditorPrefs.GetString(key); var rectValues rectStr.Split(,); if (rectValues.Length 4) { _savedWindowRect new Rect( float.Parse(rectValues[0]), float.Parse(rectValues[1]), float.Parse(rectValues[2]), float.Parse(rectValues[3]) ); this.position _savedWindowRect; } } else { // 首次打开居中显示 var mainWindow EditorGUIUtility.GetMainWindowPosition(); var center new Vector2(mainWindow.width / 2, mainWindow.height / 2); var size DefaultSize; this.position new Rect(center.x - size.x / 2, center.y - size.y / 2, size.x, size.y); } } // 保存窗口状态 private void SaveWindowState() { string key $MyPluginFramework_WindowRect_{WindowId}; var rect this.position; string rectStr ${rect.x},{rect.y},{rect.width},{rect.height}; EditorPrefs.SetString(key, rectStr); } // 编辑器每帧更新子类可重写以实现动画等效果 protected virtual void OnEditorUpdate() { } // 绘制窗口内容的主入口 protected virtual void OnGUI() { // 重置布局Y轴起点 CurrentLayoutY 0; // 绘制工具栏 DrawToolbar(); // 留出工具栏空间 CurrentLayoutY EditorGUIUtility.singleLineHeight 10; // 绘制主体内容区域 DrawContent(); } // 绘制顶部工具栏可重写 protected virtual void DrawToolbar() { GUILayout.BeginHorizontal(EditorStyles.toolbar); { // 示例窗口标题 GUILayout.Label(this.titleContent.text, EditorStyles.boldLabel); GUILayout.FlexibleSpace(); // 占位符将后续按钮推到右边 if (GUILayout.Button(Refresh, EditorStyles.toolbarButton)) { OnRefresh(); } if (GUILayout.Button(Settings, EditorStyles.toolbarButton)) { OnOpenSettings(); } } GUILayout.EndHorizontal(); } // 绘制主体内容必须由子类重写 protected virtual void DrawContent() { EditorGUILayout.HelpBox(Override DrawContent() to implement your windows UI., MessageType.Info); } // 工具栏按钮回调 protected virtual void OnRefresh() { Debug.Log($[{WindowId}] Refresh clicked.); // 通常这里会触发数据重新加载 Repaint(); // 重绘UI } protected virtual void OnOpenSettings() { // 可以打开一个设置窗口或者聚焦到Project Settings中的对应项 SettingsService.OpenProjectSettings(Project/MyPluginFramework); } // 布局辅助方法 // 开始一个垂直布局块并自动管理Y轴偏移 protected void BeginLayoutArea(float height) { GUILayout.BeginArea(new Rect(LeftMargin, CurrentLayoutY, position.width - LeftMargin - RightMargin, height)); } protected void EndLayoutArea() { GUILayout.EndArea(); CurrentLayoutY GUILayoutUtility.GetLastRect().height ElementSpacing; } // 快速绘制一个带标签的字段 protected T LayoutFieldT(string label, T value) { BeginLayoutArea(EditorGUIUtility.singleLineHeight); var result default(T); if (typeof(T) typeof(string)) { result (T)(object)EditorGUILayout.TextField(label, (string)(object)value); } else if (typeof(T) typeof(int)) { result (T)(object)EditorGUILayout.IntField(label, (int)(object)value); } // ... 可以扩展更多类型 else { EditorGUILayout.LabelField(label, Unsupported field type); } EndLayoutArea(); return result; } }这个基类已经做了很多事自动保存/恢复窗口位置、提供了简单的流式布局辅助方法、内置了通用工具栏。现在用它来创建一个工具窗口将变得非常简单。3.3 第三步使用框架快速实现一个批量重命名工具让我们用这个框架快速实现一个网络热词中提到的“Unity编辑器物体批量添加组件”的简化版——批量重命名工具。// BatchRenameWindow.cs using UnityEditor; using UnityEngine; using System.Text.RegularExpressions; public class BatchRenameWindow : BaseEditorWindow { // 工具特定的数据 private string _searchPattern ; private string _replaceWith ; private bool _useRegex false; private bool _renameChildren false; // 重写窗口ID用于独立存储状态 protected override string WindowId MyPluginFramework.BatchRenameWindow; // 添加菜单项 [MenuItem(Tools/MyPlugin Framework/Batch Rename)] public static void ShowWindow() { var window GetWindowBatchRenameWindow(); window.titleContent new GUIContent(Batch Rename); window.Show(); } // 重写内容绘制 protected override void DrawContent() { // 使用基类的布局方法 EditorGUILayout.LabelField(Batch Rename Tool, EditorStyles.largeLabel); CurrentLayoutY 24; // 大标签的高度 // 输入区域 _searchPattern LayoutField(Search Pattern (or text to replace), _searchPattern); _replaceWith LayoutField(Replace With, _replaceWith); // 选项区域 BeginLayoutArea(EditorGUIUtility.singleLineHeight * 3); _useRegex EditorGUILayout.ToggleLeft(Use Regular Expression (Advanced), _useRegex); _renameChildren EditorGUILayout.ToggleLeft(Also rename children of selected objects, _renameChildren); EditorGUILayout.Space(); EndLayoutArea(); // 操作按钮区域 BeginLayoutArea(EditorGUIUtility.singleLineHeight * 2); GUILayout.BeginHorizontal(); { GUI.enabled Selection.gameObjects.Length 0 !string.IsNullOrEmpty(_searchPattern); if (GUILayout.Button(Rename Selected Objects, GUILayout.Height(30))) { PerformRename(); } GUI.enabled true; if (GUILayout.Button(Reset, GUILayout.Width(60))) { ResetFields(); } } GUILayout.EndHorizontal(); EndLayoutArea(); // 状态信息区域 BeginLayoutArea(EditorGUIUtility.singleLineHeight * 2); EditorGUILayout.HelpBox($Selected Objects: {Selection.gameObjects.Length}, MessageType.None); if (!string.IsNullOrEmpty(_searchPattern) _useRegex) { try { var regex new Regex(_searchPattern); EditorGUILayout.HelpBox(Regex pattern is valid., MessageType.Info); } catch (System.Exception e) { EditorGUILayout.HelpBox($Invalid Regex: {e.Message}, MessageType.Error); } } EndLayoutArea(); } private void PerformRename() { if (Selection.gameObjects.Length 0) { EditorUtility.DisplayDialog(Error, No objects selected!, OK); return; } // 开始一个可撤销的操作组 Undo.RecordObjects(Selection.gameObjects, Batch Rename); int renameCount 0; foreach (GameObject go in Selection.gameObjects) { string oldName go.name; string newName oldName; if (_useRegex) { try { newName Regex.Replace(oldName, _searchPattern, _replaceWith); } catch (System.Exception e) { Debug.LogError($Regex error on {oldName}: {e.Message}); continue; } } else { newName oldName.Replace(_searchPattern, _replaceWith); } if (oldName ! newName) { go.name newName; renameCount; // 如果需要递归重命名子物体 if (_renameChildren) { RenameChildrenRecursive(go.transform, _searchPattern, _replaceWith, _useRegex); } } } // 标记场景为已修改 UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(UnityEngine.SceneManagement.SceneManager.GetActiveScene()); Debug.Log($[BatchRename] Renamed {renameCount} object(s).); EditorUtility.DisplayDialog(Complete, $Renamed {renameCount} object(s)., OK); } private void RenameChildrenRecursive(Transform parent, string search, string replace, bool useRegex) { foreach (Transform child in parent) { string oldName child.name; string newName useRegex ? Regex.Replace(oldName, search, replace) : oldName.Replace(search, replace); if (oldName ! newName) { Undo.RecordObject(child.gameObject, Rename Child); child.name newName; } RenameChildrenRecursive(child, search, replace, useRegex); } } private void ResetFields() { _searchPattern ; _replaceWith ; _useRegex false; _renameChildren false; } // 重写刷新方法当选择集变化时自动更新UI protected override void OnRefresh() { base.OnRefresh(); // 这里可以添加更复杂的数据刷新逻辑比如从选择集中预填充搜索模式 } }通过继承BaseEditorWindow我们只用了百来行代码就实现了一个功能完整、支持撤销、支持正则表达式、UI布局清晰的批量重命名工具。这就是框架带来的效率提升。4. 框架进阶处理复杂需求与性能优化4.1 支持UIElements面向未来的UI系统虽然IMGUI在快速原型和编辑器扩展中依然强大但Unity正在大力推广UIElements作为新的UI系统它更适合构建复杂、样式丰富的编辑器窗口。一个现代的插件框架应该考虑对UIElements的支持。我们可以创建一个BaseUIElementsWindow它继承自EditorWindow但使用UIElements来构建界面。框架可以提供一套预定义的USSUnity Style Sheets样式表和UXML模板确保所有插件拥有一致的视觉风格。// BaseUIElementsWindow.cs using UnityEditor; using UnityEngine; using UnityEngine.UIElements; public class BaseUIElementsWindow : EditorWindow { protected VisualElement rootVisualElement; protected string stylesheetPath Assets/MyPluginFramework/Editor/Resources/FrameworkStyles.uss; protected string mainTemplatePath Assets/MyPluginFramework/Editor/Resources/MainTemplate.uxml; protected virtual void OnEnable() { // 加载视觉树和样式表 rootVisualElement rootVisualElement ?? this.rootVisualElement; // 确保root存在 var visualTree AssetDatabase.LoadAssetAtPathVisualTreeAsset(mainTemplatePath); var styleSheet AssetDatabase.LoadAssetAtPathStyleSheet(stylesheetPath); if (visualTree ! null) { visualTree.CloneTree(rootVisualElement); } if (styleSheet ! null) { rootVisualElement.styleSheets.Add(styleSheet); } // 查找并绑定UI元素 BindUIElements(); } // 子类重写此方法来绑定UI元素和事件 protected virtual void BindUIElements() { } }然后我们可以创建一个对应的USS文件来定义颜色、字体、间距等以及一个UXML文件作为基础布局模板。这样插件开发者只需要关注自己工具特有的UI逻辑和业务逻辑即可。4.2 异步操作与进度反馈编辑器插件在执行耗时操作如遍历整个项目的纹理资产、批量处理大量预制体时必须避免阻塞主线程否则会导致Unity编辑器无响应。框架需要提供标准的异步任务和进度显示方案。可以利用EditorApplication.update回调模拟协程或者直接使用C#的async/await配合IProgressT接口。框架可以提供一个通用的ProgressWindow或ProgressBar组件。// AsyncProcessor.cs (框架提供的工具类) using UnityEditor; using UnityEngine; using System; using System.Threading.Tasks; public static class AsyncProcessor { public static async Task ProcessWithProgressT(string title, string info, FuncIProgressfloat, TaskT taskFunc) { int progressId Progress.Start(title, info, Progress.Options.Sticky); try { var progress new Progressfloat(p Progress.Report(progressId, p)); var result await taskFunc(progress); Progress.Finish(progressId, Progress.Status.Succeeded); return result; } catch (Exception e) { Progress.Finish(progressId, Progress.Status.Failed); Debug.LogError($Async task failed: {e.Message}); throw; } } }在插件中可以这样使用// 在某个工具窗口中 private async void OnProcessAllTexturesClicked() { try { var processedCount await AsyncProcessor.ProcessWithProgressint( Processing Textures, Compressing and analyzing..., async (progress) { int count 0; var allTextures Resources.FindObjectsOfTypeAllTexture2D(); float total allTextures.Length; for (int i 0; i allTextures.Length; i) { // 模拟耗时处理 await Task.Delay(10); // ... 实际处理逻辑 ... count; progress.Report(i / total); // 更新进度 } return count; }); Debug.Log($Processed {processedCount} textures.); } catch (Exception e) { EditorUtility.DisplayDialog(Error, e.Message, OK); } }4.3 插件模块化与动态加载对于大型框架我们希望插件能够模块化即插即用。这可以通过程序集定义文件asmdef和反射来实现。框架可以定义一个公共接口IPluginModule// IPluginModule.cs public interface IPluginModule { string ModuleName { get; } string Version { get; } void OnInitialize(); // 模块初始化 void OnGUI(); // 如果模块有UI在此绘制 void OnShutdown(); // 模块关闭清理 }框架在启动时扫描项目中所有实现了IPluginModule的类并实例化、初始化它们。这样新的功能可以以独立程序集的形式开发只需引用框架的核心程序集并在主框架的“插件管理”窗口中动态启用或禁用。5. 常见问题、调试技巧与避坑指南即使有了框架开发插件时依然会遇到各种坑。以下是一些高频问题和解决思路。5.1 序列化与数据持久化陷阱问题自定义的窗口类或数据类字段值在编辑器重启后丢失。原因没有正确使用序列化。Unity不会自动序列化非Serializable类或某些类型的字段如静态字段、属性。解决方案给需要保存数据的类加上[System.Serializable]特性。对于编辑器窗口的状态使用EditorPrefs适用于简单键值对或ScriptableObject资产适用于复杂数据结构。使用SerializedObject和SerializedProperty来访问和修改Unity引擎对象的序列化字段这是最规范的方式能自动处理撤销和多对象编辑。// 使用SerializedObject的正确姿势 private SerializedObject _serializedObject; private SerializedProperty _someProperty; void OnEnable() { // target 是Editor脚本中自动关联的对象 _serializedObject new SerializedObject(target); _someProperty _serializedObject.FindProperty(someField); } void OnInspectorGUI() { _serializedObject.Update(); // 将目标对象的数据拉取到SerializedObject EditorGUILayout.PropertyField(_someProperty); if (_serializedObject.ApplyModifiedProperties()) // 将修改应用回目标对象并自动处理撤销 { // 数据已修改 } }5.2 编辑器性能优化问题插件窗口在打开或操作时导致编辑器卡顿。排查与解决避免在OnGUI中执行耗时操作OnGUI每帧可能调用多次。任何文件IO、复杂计算、FindObjectsOfType等操作都应移到OnEnable或由按钮事件触发并缓存结果。使用EditorGUIUtility.SetWantsMouseJumping对于可滚动列表设置这个为1可以改善滚动性能。虚拟化长列表如果列表有成百上千项只绘制视口内的项。这是UIElements的ListView的默认行为在IMGUI中需要手动实现。善用EditorApplication.delayCall将非紧急的初始化工作推迟到下一帧避免阻塞当前帧的渲染。5.3 与Unity版本兼容性问题插件在某个Unity版本上工作正常在另一个版本上报错或功能异常。策略使用版本编译指令利用#if UNITY_XXXX来编写特定版本的代码。#if UNITY_2021_2_OR_NEWER // 使用2021.2及以上版本的新API var something NewApi.DoSomething(); #else // 旧版本的备用方案 var something LegacyApi.DoSomething(); #endif明确声明支持的版本在插件的说明文档或package.json如果是UPM包中清晰写明测试通过的Unity版本范围。API兼容性级别将项目的.NET或Mono兼容性级别设置为一个较旧、稳定的版本如.NET Standard 2.0以最大化兼容性。5.4 打包与分发注意事项问题自己测试好好的插件发给别人用就各种报错比如找不到类型、资产丢失。标准化流程使用程序集定义为你的插件代码创建asmdef文件并明确设置依赖。这能加快编译速度并避免命名空间冲突。资源路径处理永远不要使用绝对路径。使用Application.dataPath结合相对路径或者更好的方式是使用AssetDatabase的GUID系统来标识和加载资源。创建完整的.unitypackage在导出包时确保包含了所有必要的运行时脚本、编辑器脚本、asmdef文件、示例场景、文档和图标。在导入时它们应该被放置在合理的文件夹结构中如Plugins/MyPlugin/EditorPlugins/MyPlugin/Runtime。提供清晰的安装说明写一个简单的README.txt或README.md说明最低Unity版本、安装步骤和快速入门指南。开发Unity插件框架是一个迭代的过程它随着你开发的插件数量增多和复杂度提升而不断演进。不要试图一开始就设计一个完美的大而全的框架而是从解决你当前最痛的痛点开始抽象出第一个可复用的模块。然后在开发下一个插件时思考如何将通用部分融入或重构到框架中。久而久之你就会拥有一套高度定制化、能极大提升自己和工作效率的“神兵利器”。记住最好的框架不是设计出来的而是在实践中生长出来的。