React / Redux中的插件/小部件模式动态添加组件

时间:2018-02-16 13:48:42

标签: reactjs ecmascript-6 redux plugin-pattern

我有一个使用React / Redux的应用程序。我使用Electron将其打包为独立应用程序。我目前正在构建基础,以允许用户将自己的插件或小部件添加到应用程序。由于我使用Redux,我希望继续使用Redux来跟踪安装的内容以及它为应用程序带来的更改/新功能。

在深入探讨我的问题之前,让我们举个例子。我的核心应用程序有一个带有"控制"的侧边栏。纽扣。核心有两个按钮,但我想让插件在那里添加自己的按钮。当然,手动我可以继续,在按钮侧边栏组件的js文件中导入插件并手动添加按钮,但这会破坏可插拔性的目的。因此,我能想到的最好和最简单的解决方案是从插件代码中调度一个动作,例如ADD_SIDEBAR_BUTTON,它基本上将一个带有按钮元数据的对象添加到一个按钮数组(包括那些核心按钮)。 像这样:

store.dispatch({type: "ADD_SIDEBAR_BUTTON", payload: {type: "button", path: "goSomewhere", label: "A label"}});

虽然这可以工作,但它限制了按钮可以做的很多,因为按钮的实际逻辑和jsx将来自核心应用程序。 在实践中,我只是渴望让插件传递一个React组件,这将使插件在事件,生命周期的完全控制等方面具有很大的自由度......但是传递除了普通对象之外的任何东西都是一个Redux no-no。

另一个解决方案是让插件开发人员创建一个带有导出类的文件,并使用redux提供它的路径,如下所示:

class MyPluginButton extends Component {
   handleClick() => evt => {
     // do my special things
   }
   render() {
     return <button onClick={this.handleClick}>My Special Button</button>
   }
}

export default MyPluginButton;

然后插件的initialize()函数将调度操作并传递核心导入按钮所需的信息(如果没有正确完成,我也可以考虑安全问题):

store.dispatch({type: "ADD_SIDEBAR_BUTTON", payload: {path: "myplugin/src/components/MyPluginButton"}});

现在我的问题是,有没有人有好的想法如何实现/应该实施?我想确保我没有错过任何其他模式或实现。

1 个答案:

答案 0 :(得分:0)

我最终做的是一个pluginRegistration模块,如下所示,在博客上找到的基本想法我再也找不到了:

const _registeredComponents = {};

export const registerComponent = (pluginName, component, injectAction) => {

  _registeredComponents[`plugin_${pluginName}_${component.PLUGIN_COMPONENT_NAME}`] = {pluginName: pluginName, component: component, action: injectAction};

};

export const getRegisteredComponents = () => {
   return {..._registeredComponents};
};

export const getRegisteredComponent = fullPluginComponentName => {
   return _registeredComponents[fullPluginComponentName].component;
}

想要添加的组件使用上面的函数注册自己并传递一个动作。从我在ComponentDidMount中的主要App组件,然后循环遍历_registeredComponents中的所有条目,调度沿组件类ref传递的操作,将我的按钮(在商店中,键字符串)添加到现有的ControlButtons组件。之后,父控件按钮组件从状态获取fullPluginComponentName,并从引用中实例化组件。瞧,使用Redux进行完全动态的React组件实例化/注入,但只保留商店中的普通对象。

我的按钮组件非常普通,除了添加了静态getter(使用constructor.name很诱人,但是当缩小JS时,名称会丢失并且可能会导致问题。)

export class MyButtonComponent extends Component {
  static get PLUGIN_COMPONENT_NAME() {
    return "MyButtonComponent";
  }

  render() {
   return (<button>My Useless Button</button>);
  }
}

在我创建我的按钮以注入React类的同一个文件中,我只需注册该组件,如下所示:

registerComponent("MyPluginName", MyButtonComponent, actions.addButtonToControls);

这将插件名称,实际组件类引用和我想要触发的操作绑定在一起。

为了获取可供使用的组件条目的键,我的主App组件在安装后循环注册组件条目:

class App extends Component {
   componentDidMount() {
       let pluginComponents = getRegisteredComponents();
       for (let fullPluginComponentName in pluginComponents) {
           let entry = pluginComponents[fullPluginComponentName];
           this.props.dispatch({type: entry.action, payload: {pluginName: entry.pluginName, fullPluginNameComponent: fullPluginComponentName}}); 
       }
   }
   render() { ... }
}

这将有效的方法是将组件键添加到商店中,以便稍后在ControlButtons(一个动态加载按钮数组的容器)中获取类引用。 这是后者如何从商店加载它们的简要版本:

class ControlButtons extends Component {
    getDynamicButtons() {
        // this.props.dynamicButtons is an array stored in redux and populated from the looped dispatch-es calls for each registered component.
        return this.props.dynamicButtons.map(componentEntry => {
            let MyDynamicButton = getRegisteredComponent(componentEntry.fullPluginComponentName);
            return <MyDynamicButton />;
        });
    }
    render() {
      <div>
         {this.getDynamicButtons()}
      </div>
    }
}

当然,不知何故,必须导入包含按钮类定义的文件。这是我旅程的下一步,我可能还会拥有一系列模块/文件名,并使用require加载它们。但这是另一个故事。