在React应用程序中拥有服务


176

我来自一个有角的世界,在那里我可以将逻辑提取到服务/工厂,并在控制器中使用它们。

我试图了解如何在React应用程序中实现相同的目标。

假设我有一个可以验证用户密码输入(强度)的组件。它的逻辑非常复杂,因此我不想将其编写在自己的组件中。

我应该在哪里写这个逻辑?在商店中,如果我使用助焊剂?还是有更好的选择?


您可以使用一个程序包,看看他们的工作方式-npmjs.com/package/react-password-strength-meter
James111 2016年

11
密码强度只是一个例子。我在寻找一个更一般的最佳实践
丹尼斯Nerush

您可能必须在服务器端执行此操作?
James111 '16

2
不可以。只有客户端逻辑不能直接在组件中。密码强度检查器仅是一个示例
Dennis Nerush

4
如果您有许多这样的功能,则可以将它们存储在帮助文件中,只需将其放入组件文件中即可使用。如果它是仅与该组件相关的单个函数,则无论复杂性如何,它都应该存在。
Jesse Kernaghan

Answers:


60

第一个答案并不反映当前的Container vs Presenter范例。

如果您需要执行某些操作(例如验证密码),则可以使用执行此操作的功能。您将把该函数作为道具传递给可重用的视图。

货柜

因此,正确的方法是编写一个ValidatorContainer,它将具有该功能的属性,并将其包装在其中,然后将正确的道具传递给孩子。当涉及到视图时,验证器容器将包装您的视图,而视图将使用容器逻辑。

验证可以全部在容器的属性中完成,但是如果您使用的是第三方验证器或任何简单的验证服务,则可以将该服务用作容器组件的属性,并在容器的方法中使用它。我已经为宁静的组件完成了此任务,并且效果很好。

提供者

如果需要更多配置,则可以使用提供者/消费者模型。提供程序是一个高级组件,它包装在顶层应用程序对象(您安装的对象)附近和下方,并将其自身的一部分或在顶层配置的属性提供给上下文API。然后,我将容器元素设置为使用上下文。

父/子上下文关系不必彼此靠近,只是必须以某种方式将子代降落。Redux存储和React Router以这种方式起作用。我用它为我的rest容器提供了一个根源的rest上下文(如果我不提供自己的)。

(请注意:上下文API在文档中被标记为实验性的,但考虑到正在使用的内容,我认为它不再适用了)。

//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使用此方法来影响历史记录的状态。

//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)
  }
}


很好的回答伙伴,您阻止了我全力以赴8)KUDOS!
csomakk

容器示例的用途是什么?
–ensei

我不主张这样做,但是如果您想沿着服务定位器的路径走(类似于Angular),则可以添加某种“注入器/容器”提供程序,以便从中解析服务(之前已经注册了它们)。
eddiewould

React钩子可以解救。使用Hooks可以编写可重用的逻辑,而无需编写类。reactjs.org/docs/...
拉贾·马利克

102

当您意识到Angular服务只是一个提供一组上下文无关方法的对象时,问题变得非常简单。只是Angular DI机制使它看起来更加复杂。DI非常有用,因为它可以为您创建和维护实例,但您实际上并不需要它。

考虑一个流行的AJAX库axios(您可能已经听说过):

import axios from "axios";
axios.post(...);

它不是服务吗?它提供了一组负责某些特定逻辑的方法,并且独立于主代码。

您的示例案例是关于创建一组隔离的方法来验证您的输入(例如,检查密码强度)。有些人建议将这些方法放在组件中,这对我来说显然是一种反模式。如果验证涉及进行和处理XHR后端调用或进行复杂的计算该怎么办?您会将这种逻辑与鼠标单击处理程序和其他特定于UI的东西混合使用吗?废话。与容器/ HOC方法相同。包装您的组件只是为了添加一种方法,该方法将检查值中是否包含数字?来吧。

我将创建一个名为“ ValidationService.js”的新文件,并将其组织如下:

const ValidationService = {
    firstValidationMethod: function(value) {
        //inspect the value
    },

    secondValidationMethod: function(value) {
        //inspect the value
    }
};

export default ValidationService;

然后在您的组件中:

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文件。

您可能需要更复杂的服务,具体取决于其他服务。在这种情况下,您的服务文件可能会返回类构造函数而不是静态对象,因此您可以自己在组件中创建该对象的实例。您也可以考虑实现一个简单的单例,以确保整个应用程序中始终只使用一个服务对象实例。


3
我也是这样做的。我很惊讶这个答案投票很少,因为这似乎是摩擦最小的方法。如果您的服务依赖于其他服务,那么它将再次通过其模块导入那些其他服务。而且,按照定义,模块是单例的,因此实际上并不需要“将其实现为简单的单例”的进一步工作-您可以免费获得该行为:)
Mickey Puri

6
+1-如果您仅使用提供功能的服务,那就是不错的答案。但是,Angular的服务是一次定义的类,因此提供的功能比仅提供功能要多。例如,您可以将对象缓存为服务类参数。
Nino Filiu

6
这应该是真正的答案,而不是上面的复杂回答
user1807334 '19

1
这是一个很好的答案,只是它不是“反应性的”。DOM将不会在服务中的变量更改上更新。
Defacto

9
但是依赖注入呢?除非以某种方式注入该服务,否则无法在您的组件中模拟该服务。也许拥有一个将每个服务作为一个字段的顶级“容器”全局对象可以解决此问题。然后,在测试中,您可以使用要模拟的服务的模拟覆盖容器字段。
menehune23 '19

34

我需要一些格式化逻辑在多个组件之间共享,并且作为Angular开发人员,自然也倾向于服务。

我通过将逻辑放在单独的文件中来共享逻辑

function format(input) {
    //convert input to output
    return output;
}

module.exports = {
    format: format
};

然后将其导入为模块

import formatter from '../services/formatter.service';

//then in component

    render() {

        return formatter.format(this.props.data);
    }

8
这是一个好主意,甚至在React文档中也提到过:reactjs.org/docs/composition-vs-inheritance.html 如果要在组件之间重用非UI功能,建议将其提取到单独的JavaScript模块中。组件可以导入它并使用该函数,对象或类,而无需扩展它。
user3426603

这实际上是唯一有意义的答案。
Artem Novikov

33

请记住,React的目的是更好地耦合逻辑上应该耦合的事物。如果您要设计一个复杂的“验证密码”方法,应该在哪里使用?

那么,每次用户需要输入新密码时,您都将需要使用它。这可以在注册屏幕,“忘记密码”屏幕,管理员“为另一个用户重置密码”屏幕等上。

但是在任何一种情况下,它总是与某些文本输入字段相关联。这就是应该将其耦合的地方。

制作一个非常小的React组件,该组件仅由输入字段和关联的验证逻辑组成。在所有可能需要输入密码的表单中输入该组件。

它与为逻辑提供服务/工厂本质上是相同的结果,但是您将其直接耦合到输入。因此,由于它永久地绑定在一起,因此您现在无需告诉该函数在哪里寻找其验证输入。


11
耦合逻辑和UI是不好的做法。为了更改逻辑,我将不得不接触组件
Dennis Nerush

14
从根本上应对您所做的假设挑战。与传统的MVC架构形成鲜明对比。 该视频很好地解释了这是为什么(相关部分大约在2分钟后开始)。
杰克罗比

8
如果还需要将相同的验证逻辑应用于文本区域元素怎么办?逻辑仍然需要提取到共享文件中。我认为开箱即用的库不具有任何等效性。Angular Service是可注入的,并且Angular框架是建立在依赖项注入设计模式之上的,该模式允许Angular管理依赖项的实例。注入服务时,通常在提供的范围内只有一个单例,以便在React中具有相同的服务,需要将第三方DI lib引入应用程序。
Downhillski

15
@gravityplanx我喜欢使用React。这不是角度模式,这是软件设计模式。我喜欢保持开放的态度,同时从其他方面借用我喜欢的东西。
Downhillski'5

1
@MickeyPuri ES6模块与依赖注入不同。
Spock

12

我也来自Angular.js领域,React.js中的服务和工厂更加简单。

您可以像我一样使用简单的函数或类,回调样式和事件Mobx :)

// 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 <div>{this.state.data}</div>;
  }
}

ReactDOM.render(<ReactApp />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>
<body>
  
  <div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>

</body>
</html>

这是一个简单的例子:


React.js是用于呈现和组织UI组件的UI库。当涉及可以帮助我们添加其他功能的服务时,我们应该创建功能,功能对象或类的集合。我发现类非常有用,但是我知道我也在玩功能风格,也可以用来创建帮助程序来添加Reac.js范围之外的有利功能。
朱拉杰

刚刚实施。将其设置为类并导出它的方式非常优雅。
贝尔森

10

相同的情况:完成了多个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经验。考虑一下逻辑的性质:

  1. 它是否(仅)绑定到UI?将其移动到组件中(可接受的答案)。
  2. 它(仅)与状态管理联系在一起吗?将其移入一个大块
  3. 绑在一起?移动到单独的文件,通过选择器在组件中使用,并进行转换。

有些还可以重用HOC,但对我来说,以上内容涵盖了几乎所有用例。另外,考虑使用鸭子来扩展状态管理,以使关注点分离并以用户界面为中心。


恕我直言,我认为提供直通DI服务一个简单的方法,通过使用ES6模块系统
米奇普里

1
@ MickeyPuri,ES6模块DI不会包含Angular DI的层次结构,即。父级(在DOM中)实例化并覆盖提供给子级组件的服务。Imho ES6模块DI与后端DI系统(例如Ninject和Structuremap)的比较接近,它们与DOM组件层次结构分开而不是基于DOM组件层次结构。但我想听听您对此的想法。
花冠

6

我也来自Angular并尝试React,到目前为止,一种推荐的(?)方式似乎正在使用High-Order Components

高阶组件(HOC)是React中用于重用组件逻辑的高级技术。HOC本身不是React API的一部分。它们是从React的组成性质中出现的一种模式。

假设您具有input并且textarea喜欢应用相同的验证逻辑:

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来验证和包装组件的样式:

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共享相同的验证行为:

const InputWithValidator = withValidator(Input)
const TextAreaWithValidator = withValidator(TextArea)

render((
  <div>
    <InputWithValidator />
    <TextAreaWithValidator />
  </div>
), document.getElementById('root'));

我创建了一个简单的 演示

编辑:另一个演示使用props传递函数数组,以便您可以跨HOCs 共享由多个验证函数组成的逻辑,例如:

<InputWithValidator validators={[validator1,validator2]} />
<TextAreaWithValidator validators={[validator1,validator2]} />

Edit2:React 16.8+提供了新功能Hook,这是共享逻辑的另一种不错的方法。

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
  }
}

https://stackblitz.com/edit/react-shared-validation-logic-using-hook?file=index.js


谢谢。我真的从这个解决方案中学到了东西。如果我需要多个验证者怎么办?例如,除了3个字母验证器之外,如果我想拥有另一个确保没有输入数字的验证器,该怎么办。我们可以组成验证器吗?
Youssef Sherif '18

1
@YoussefSherif您可以准备多个验证功能,并将其作为的属性传递HOC,有关其他演示,请参见我的编辑。
鲍勃,

那么HOC基本上是容器的组成部分?
–ensei

是的,来自React doc:“请注意,HOC不会修改输入组件,也不会使用继承来复制其行为。相反,HOC通过将其包装在容器组件中来构成原始组件。HOC是纯具有零副作用的功能。”
鲍勃,

1
要求是注入逻辑,我不明白为什么我们需要HOC来执行此操作。尽管您可以使用HOC进行操作,但是感觉太复杂了。我对HOC的理解是,何时还需要添加和管理一些其他状态,即不是纯逻辑(此处就是这种情况)。
米奇·普里

4

服务不限于Angular,即使在Angular2 +也是如此

服务只是助手功能的集合...

有很多方法可以创建它们并在整个应用程序中重复使用它们。

1)它们可以是从js文件导出的所有独立功能,如下所示:

export const firstFunction = () => {
   return "firstFunction";
}

export const secondFunction = () => {
   return "secondFunction";
}
//etc

2)我们也可以使用工厂方法,例如,具有功能集合... ES6,它可以是类而不是函数构造函数:

class myService {

  constructor() {
    this._data = null;
  }

  setMyService(data) {
    this._data = data;
  }

  getMyService() {
    return this._data;
  }

}

在这种情况下,您需要使用新密钥创建一个实例...

const myServiceInstance = new myService();

同样在这种情况下,每个实例都有自己的生命,因此如果要共享它,请小心,在这种情况下,应仅导出所需的实例...

3)如果您的函数和实用程序无法共享,您甚至可以将它们放在React组件中,就像在react组件中的函数一样。

class Greeting extends React.Component {
  getName() {
    return "Alireza Dezfoolian";
  }

  render() {
    return <h1>Hello, {this.getName()}</h1>;
  }
}

4)处理事情的另一种方式,可能是使用Redux,它是您的临时存储,因此,如果您在React应用程序中拥有它,它可以帮助您实现许多getter setter函数 ...这就像一个大商店可以跟踪您的状态并可以在您的各个组件之间共享状态,因此可以消除我们在服务中使用的吸气剂安装工的许多麻烦...

做一个DRY代码而不重复需要使代码可重用和可读的东西总是很好的,但是不要尝试在React应用程序中遵循Angular的方法像第4条中提到的那样,使用Redux可以减少对服务,并且您限制将它们用于某些可重用的辅助功能,例如项目1 ...


当然,您可以在我的个人网站上找到它,该网站是我的个人资料页面上的链接...
Alireza

“不要在React中遵循Angular的方式” .. ahem Angular推广了使用Redux的功能,并使用Observables和类似Redux的状态管理(如RxJS / Store)将存储流式传输到表示性组件。..你是说AngularJS吗?因为那是另一回事
Spock

1

我和你一样穿着靴子。在您提到的情况下,我将输入验证UI组件实现为React组件。

我同意验证逻辑本身的实现不应(必须)耦合。因此,我将其放入单独的JS模块中。

也就是说,对于不应该耦合的逻辑,请在单独的文件中使用JS模块/类,并使用require / import将组件与“服务”分离。

这允许依赖项注入和两者的单元测试独立。


1

或者您可以将类继承“ http”注入React Component

通过道具对象。

  1. 更新:

    ReactDOM.render(<ReactApp data={app} />, document.getElementById('root'));
  2. 像这样简单地编辑React Component ReactApp:

    class ReactApp extends React.Component {
    
    state = {
    
        data: ''
    
    }
    
        render(){
    
        return (
            <div>
            {this.props.data.getData()}      
            </div>
    
        )
        }
    }

0

我遇到的最常用的可重用逻辑模式是编写钩子或创建utils文件。这取决于您要完成的工作。

hooks/useForm.js

就像您要验证表单数据一样,我将创建一个名为useForm.js的自定义钩子,并提供表单数据,作为回报,它将返回一个包含以下内容的对象:

Object: {
    value,
    error,
}

您一定可以从中获得更多收益。

utils/URL.js

另一个示例就像您要从URL提取一些信息,然后为该文件创建一个包含函数的utils文件,并在需要时将其导入:

 export function getURLParam(p) {
...
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.