Having services in React application
我来自角度世界,在那里我可以将逻辑提取到服务/工厂并在我的控制器中使用它们。
我试图了解如何在React应用程序中实现相同的功能。
假设我有一个验证用户密码输入的组件(它的强度)。 它的逻辑非常复杂,因此我不想在它自己的组件中编写它。
我应该在哪里写这个逻辑? 在商店里,如果我使用助焊剂? 或者有更好的选择吗?
当您意识到Angular服务只是一个提供一组与上下文无关的方法的对象时,问题变得非常简单。它只是Angular DI机制,使它看起来更复杂。 DI很有用,因为它负责为您创建和维护实例,但您并不真正需要它。
考虑一个名为axios的流行AJAX库(您可能已经听说过):
1 2 | import axios from"axios"; axios.post(...); |
它不是一种服务吗?它提供了一组负责某些特定逻辑的方法,并且与主代码无关。
您的示例案例是关于创建一组用于验证输入的隔离方法(例如,检查密码强度)。有人建议将这些方法放在组件中,这对我来说显然是一种反模式。如果验证涉及制作和处理XHR后端调用或进行复杂计算,该怎么办?您会将此逻辑与鼠标单击处理程序和其他UI特定的东西混合使用吗?废话。与容器/ HOC方法相同。包装组件只是为了添加一个方法来检查值中是否有数字?来吧。
我只想创建一个名为'ValidationService.js'的新文件,并按如下方式组织它:
1 2 3 4 5 6 7 8 9 10 11 | const ValidationService = { firstValidationMethod: function(value) { //inspect the value }, secondValidationMethod: function(value) { //inspect the value } }; export default ValidationService; |
然后在你的组件中:
1 2 3 4 5 6 7 8 9 10 11 12 13 | import ValidationService from"./services/ValidationService.js"; ... //inside the component yourInputChangeHandler(event) { if(!ValidationService.firstValidationMethod(event.target.value) { //show a validation warning return false; } //proceed } |
在任何您想要的地方使用此服务。如果验证规则发生更改,则只需关注ValidationService.js文件。
您可能需要更复杂的服务,这取决于其他服务。在这种情况下,您的服务文件可能会返回类构造函数而不是静态对象,因此您可以在组件中自己创建对象的实例。您还可以考虑实现一个简单的单例,以确保在整个应用程序中始终只使用一个服务对象实例。
第一个答案并不反映当前的Container vs Presenter范例。
如果你需要做一些事情,比如验证密码,你可能会有一个功能来完成它。您将该功能作为道具传递给您的可重用视图。
集装箱
因此,正确的方法是编写一个ValidatorContainer,它将该函数作为属性,并将表单包装在其中,将正确的props传递给子。在您的视图中,验证器容器包装您的视图,视图使用容器逻辑。
验证可以在容器的属性中完成,但是您使用的是第三方验证器或任何简单的验证服务,您可以将该服务用作容器组件的属性并在容器的方法中使用它。我已经为宁静的组件做了这个,它运行得很好。
供应商
如果需要更多配置,您可以使用提供者/消费者模型。提供程序是一个高级组件,它包装在顶部应用程序对象(您安装的对象)附近和下方的某个位置,并将其自身的一部分或顶层中配置的属性提供给上下文API。然后我设置我的容器元素以使用上下文。
父/子上下文关系不必彼此靠近,只要孩子必须以某种方式下降。 Redux以这种方式存储和React Router功能。我用它来为我的休息容器提供一个root restful上下文(如果我不提供我自己的容器)。
(注意:上下文API在文档中标记为实验性的,但考虑到正在使用它,我不认为它已经存在了)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | //An example of a Provider component, takes a preconfigured restful.js //object and makes it available anywhere in the application export default class RestfulProvider extends React.Component { constructor(props){ super(props); if(!("restful" in props)){ throw Error("Restful service must be provided"); } } getChildContext(){ return { api: this.props.restful }; } render() { return this.props.children; } } RestfulProvider.childContextTypes = { api: React.PropTypes.object }; |
中间件
我还没有尝试过,但看过用过的另一种方法是将中间件与Redux结合使用。您可以在应用程序之外定义服务对象,或者至少高于redux存储。在存储创建期间,您将服务注入中间件,中间件处理影响服务的任何操作。
通过这种方式,我可以将restful.js对象注入中间件并用独立的操作替换我的容器方法。我仍然需要一个容器组件来为表单视图层提供操作,但是connect()和mapDispatchToProps让我在那里。
例如,新的v4 react-router-redux使用此方法来影响历史状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | //Example middleware from react-router-redux //History is our service here and actions change it. import { CALL_HISTORY_METHOD } from './actions' /** * This middleware captures CALL_HISTORY_METHOD actions to redirect to the * provided history object. This will prevent these actions from reaching your * reducer or any middleware that comes after this one. */ export default function routerMiddleware(history) { return () => next => action => { if (action.type !== CALL_HISTORY_METHOD) { return next(action) } const { payload: { method, args } } = action history[method](...args) } } |
请记住,React的目的是更好地结合逻辑上应该耦合的东西。如果您正在设计一个复杂的"验证密码"方法,它应该耦合在哪里?
那么每次用户需要输入新密码时你都需要使用它。这可以在注册屏幕上,"忘记密码"屏幕,管理员"重置另一个用户的密码"屏幕等。
但在任何一种情况下,它总是与某些文本输入字段相关联。这就是它应该耦合的地方。
创建一个非常小的React组件,它只包含一个输入字段和相关的验证逻辑。在所有可能想要输入密码的表单中输入该组件。
它与逻辑服务/工厂的结果基本相同,但是您将它直接耦合到输入。因此,您现在永远不需要告诉该函数在哪里查找它的验证输入,因为它永久地绑在一起。
我需要在多个组件之间共享一些格式化逻辑,而Angular开发人员也自然倾向于服务。
我把它放在一个单独的文件中来共享逻辑
1 2 3 4 5 6 7 8 | function format(input) { //convert input to output return output; } module.exports = { format: format }; |
然后将其作为模块导入
1 2 3 4 5 6 7 8 | import formatter from '../services/formatter.service'; //then in component render() { return formatter.format(this.props.data); } |
我也来自Angular.js区域,React.js中的服务和工厂更简单。
你可以使用普通的函数或类,回调样式和像我这样的事件Mobx :)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | // Here we have Service class > dont forget that in JS class is Function class HttpService { constructor() { this.data ="Hello data from HttpService"; this.getData = this.getData.bind(this); } getData() { return this.data; } } // Making Instance of class > it's object now const http = new HttpService(); // Here is React Class extended By React class ReactApp extends React.Component { state = { data:"" }; componentDidMount() { const data = http.getData(); this.setState({ data: data }); } render() { return {this.state.data}; } } ReactDOM.render(<ReactApp />, document.getElementById("root")); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"> <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width"> JS Bin </head> <body> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"> </body> </html> |
这是一个简单的例子:
同样的情况:完成了多个Angular项目并转向React,没有通过DI提供服务的简单方法似乎是一个缺失的部分(除了服务的细节)。
使用上下文和ES7装饰器我们可以接近:
https://jaysoo.ca/2015/06/09/react-contexts-and-dependency-injection/
似乎这些家伙已经朝着不同的方向迈进了一步:
http://blog.wolksoftware.com/dependency-injection-in-react-powered-inversifyjs
仍感觉像是在反对谷物。在进行一个重大的React项目后,将在6个月后重新审视这个答案。
编辑:6个月后回来,有更多的React经验。考虑逻辑的本质:
有些人还可以将HOC用于重复使用,但对我来说,上述内容几乎涵盖了所有用例。此外,考虑使用ducks来扩展状态管理,以使问题保持??独立并以状态UI为中心。
我也来自Angular并试用React,截至目前,一种推荐(?)方式似乎是使用高阶组件:
A higher-order component (HOC) is an advanced technique in React for
reusing component logic. HOCs are not part of the React API, per se.
They are a pattern that emerges from React’s compositional nature.
假设您有
1 2 3 4 5 6 7 8 9 10 11 | const Input = (props) => ( <input type="text" style={props.style} onChange={props.onChange} /> ) const TextArea = (props) => ( <textarea rows="3" style={props.style} onChange={props.onChange} > </textarea> ) |
然后编写一个确认和样式包装组件的HOC:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | function withValidator(WrappedComponent) { return class extends React.Component { constructor(props) { super(props) this.validateAndStyle = this.validateAndStyle.bind(this) this.state = { style: {} } } validateAndStyle(e) { const value = e.target.value const valid = value && value.length > 3 // shared logic here const style = valid ? {} : { border: '2px solid red' } console.log(value, valid) this.setState({ style: style }) } render() { return <WrappedComponent onChange={this.validateAndStyle} style={this.state.style} {...this.props} /> } } } |
现在这些HOC共享相同的验证行为:
1 2 3 4 5 6 7 8 9 | const InputWithValidator = withValidator(Input) const TextAreaWithValidator = withValidator(TextArea) render(( <InputWithValidator /> <TextAreaWithValidator /> ), document.getElementById('root')); |
我创建了一个简单的演示。
编辑:另一个演示使用props来传递一系列函数,这样你就可以在
1 2 | <InputWithValidator validators={[validator1,validator2]} /> <TextAreaWithValidator validators={[validator1,validator2]} /> |
Edit2:React 16.8+提供了一个新功能,Hook,另一种分享逻辑的好方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | const Input = (props) => { const inputValidation = useInputValidation() return ( <input type="text" {...inputValidation} /> ) } function useInputValidation() { const [value, setValue] = useState('') const [style, setStyle] = useState({}) function handleChange(e) { const value = e.target.value setValue(value) const valid = value && value.length > 3 // shared logic here const style = valid ? {} : { border: '2px solid red' } console.log(value, valid) setStyle(style) } return { value, style, onChange: handleChange } } |
即使在Angular2 +中,服务也不仅限于Angular,
服务只是辅助功能的集合......
有很多方法可以创建它们并在整个应用程序中重用它们......
1)它们可以是从js文件导出的所有分离函数,类似如下:
1 2 3 4 5 6 7 8 | export const firstFunction = () => { return"firstFunction"; } export const secondFunction = () => { return"secondFunction"; } //etc |
2)我们也可以使用工厂方法,例如,使用函数集合...使用ES6它可以是类而不是函数构造函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | class myService { constructor() { this._data = null; } setMyService(data) { this._data = data; } getMyService() { return this._data; } } |
在这种情况下,您需要使用新密钥创建一个实例...
1 | const myServiceInstance = new myService(); |
同样在这种情况下,每个实例都有它自己的生命,所以如果你想要分享它,请小心,在这种情况下你应该只导出你想要的实例...
3)如果你的函数和utils不会被共享,你甚至可以将它们放在React组件中,在这种情况下,就像你的react组件中的函数一样......
1 2 3 4 5 6 7 8 9 | class Greeting extends React.Component { getName() { return"Alireza Dezfoolian"; } render() { return Hello, {this.getName()}; } } |
4)你可以处理事情的另一种方式,可能是使用Redux,它是你的临时商店,所以如果你在你的React应用程序中有它,它可以帮助你使用你使用的许多getter setter函数...它就像一个大商店保持你的状态跟踪,并可以在你的组件之间共享,所以可以摆脱我们在服务中使用的getter setter东西的许多痛苦...
执行DRY代码并不重复需要用于使代码可重用和可读的内容总是好的,但是不要尝试在React app中遵循Angular方式,如第4项中所述,使用Redux可以减少您的需求服务,你限制使用它们为一些可重复使用的辅助函数,如第1项...
或者您可以将类继承"http"注入React组件
通过道具对象。
更新:
1 | ReactDOM.render(<ReactApp data={app} />, document.getElementById('root')); |
只需编辑React Component ReactApp,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | class ReactApp extends React.Component { state = { data: '' } render(){ return ( {this.props.data.getData()} ) } } |
我和你一样在同一个靴子里。在你提到的情况下,我会将输入验证UI组件实现为React组件。
我同意验证逻辑本身的实现应该(必须)不耦合。因此我会把它放到一个单独的JS模块中。
也就是说,对于不应该耦合的逻辑,在单独的文件中使用JS模块/类,并使用require / import将组件与"服务"分离。
这允许独立地依赖注入和单元测试两者。